diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs index 0df820dcecf22e..5c9d5b833bf012 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs @@ -120,6 +120,11 @@ public void Emit() GenerateTypeInfo(typeGenerationSpec); } + foreach (TypeGenerationSpec typeGenerationSpec in _currentContext.ImplicitlyRegisteredTypes) + { + GenerateTypeInfo(typeGenerationSpec); + } + string contextName = _currentContext.ContextType.Name; // Add root context implementation. diff --git a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs index 9590a3b90f07d2..787d270de38be8 100644 --- a/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs +++ b/src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs @@ -28,6 +28,7 @@ private sealed class Parser private const string JsonConverterFactoryFullName = "System.Text.Json.Serialization.JsonConverterFactory"; private const string JsonConverterOfTFullName = "System.Text.Json.Serialization.JsonConverter`1"; private const string JsonArrayFullName = "System.Text.Json.Nodes.JsonArray"; + private const string JsonDerivedTypeAttributeFullName = "System.Text.Json.Serialization.JsonDerivedTypeAttribute"; private const string JsonElementFullName = "System.Text.Json.JsonElement"; private const string JsonExtensionDataAttributeFullName = "System.Text.Json.Serialization.JsonExtensionDataAttribute"; private const string JsonNodeFullName = "System.Text.Json.Nodes.JsonNode"; @@ -40,7 +41,7 @@ private sealed class Parser private const string JsonPropertyNameAttributeFullName = "System.Text.Json.Serialization.JsonPropertyNameAttribute"; private const string JsonPropertyOrderAttributeFullName = "System.Text.Json.Serialization.JsonPropertyOrderAttribute"; private const string JsonSerializerContextFullName = "System.Text.Json.Serialization.JsonSerializerContext"; - private const string JsonSerializerAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute"; + private const string JsonSerializableAttributeFullName = "System.Text.Json.Serialization.JsonSerializableAttribute"; private const string JsonSourceGenerationOptionsAttributeFullName = "System.Text.Json.Serialization.JsonSourceGenerationOptionsAttribute"; private const string DateOnlyFullName = "System.DateOnly"; @@ -176,6 +177,14 @@ private sealed class Parser defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); + private static DiagnosticDescriptor PolymorphismNotSupported { get; } = new DiagnosticDescriptor( + id: "SYSLIB1039", + title: new LocalizableResourceString(nameof(SR.FastPathPolymorphismNotSupportedTitle), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.FastPathPolymorphismNotSupportedMessageFormat), SR.ResourceManager, typeof(FxResources.System.Text.Json.SourceGeneration.SR)), + category: JsonConstants.SystemTextJsonSourceGenerationName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + public Parser(Compilation compilation, in JsonSourceGenerationContext sourceGenerationContext) { _compilation = compilation; @@ -240,7 +249,7 @@ public Parser(Compilation compilation, in JsonSourceGenerationContext sourceGene { Compilation compilation = _compilation; INamedTypeSymbol jsonSerializerContextSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerContextFullName); - INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSerializerAttributeFullName); + INamedTypeSymbol jsonSerializableAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSerializableAttributeFullName); INamedTypeSymbol jsonSourceGenerationOptionsAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonSourceGenerationOptionsAttributeFullName); INamedTypeSymbol jsonConverterOfTAttributeSymbol = compilation.GetBestTypeByMetadataName(JsonConverterOfTFullName); @@ -551,7 +560,7 @@ private static bool TryGetClassDeclarationList(INamedTypeSymbol typeSymbol, [Not INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType; string fullName = attributeContainingTypeSymbol.ToDisplayString(); - if (fullName == "System.Text.Json.Serialization.JsonSerializableAttribute") + if (fullName == JsonSerializableAttributeFullName) { return classDeclarationSyntax; } @@ -694,6 +703,7 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener string? converterInstatiationLogic = null; bool implementsIJsonOnSerialized = false; bool implementsIJsonOnSerializing = false; + bool isPolymorphic = false; bool hasInitOnlyProperties = false; bool hasTypeFactoryConverter = false; bool hasPropertyFactoryConverters = false; @@ -703,7 +713,9 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener foreach (CustomAttributeData attributeData in attributeDataList) { Type attributeType = attributeData.AttributeType; - if (attributeType.FullName == JsonNumberHandlingAttributeFullName) + string attributeTypeFullName = attributeType.FullName; + + if (attributeTypeFullName == JsonNumberHandlingAttributeFullName) { IList ctorArgs = attributeData.ConstructorArguments; numberHandling = (JsonNumberHandling)ctorArgs[0].Value; @@ -718,6 +730,22 @@ private TypeGenerationSpec GetOrAddTypeGenerationSpec(Type type, JsonSourceGener forType: true, ref hasTypeFactoryConverter); } + + if (attributeTypeFullName == JsonDerivedTypeAttributeFullName) + { + Debug.Assert(attributeData.ConstructorArguments.Count > 0); + ITypeSymbol derivedTypeSymbol = (ITypeSymbol)attributeData.ConstructorArguments[0].Value; + Type derivedType = derivedTypeSymbol.AsType(_metadataLoadContext); + TypeGenerationSpec derivedTypeSpec = GetOrAddTypeGenerationSpec(derivedType, generationMode); + _implicitlyRegisteredTypes.Add(derivedTypeSpec); + + if (!isPolymorphic && generationMode == JsonSourceGenerationMode.Serialization) + { + _typeLevelDiagnostics.Add((type, PolymorphismNotSupported, new string[] { type.FullName })); + } + + isPolymorphic = true; + } } if (foundDesignTimeCustomConverter) @@ -1067,7 +1095,8 @@ void CacheMemberHelper(Location memberLocation) implementsIJsonOnSerializing : implementsIJsonOnSerializing, canContainNullableReferenceAnnotations: canContainNullableReferenceAnnotations, hasTypeFactoryConverter : hasTypeFactoryConverter, - hasPropertyFactoryConverters : hasPropertyFactoryConverters); + hasPropertyFactoryConverters : hasPropertyFactoryConverters, + isPolymorphic : isPolymorphic); return typeMetadata; } diff --git a/src/libraries/System.Text.Json/gen/Resources/Strings.resx b/src/libraries/System.Text.Json/gen/Resources/Strings.resx index 1074f0eb468179..4b0e51d66f1927 100644 --- a/src/libraries/System.Text.Json/gen/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/gen/Resources/Strings.resx @@ -165,4 +165,10 @@ The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf index 9253f60f48c030..41ca6a77babc90 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.cs.xlf @@ -32,6 +32,16 @@ Duplicitní název typu + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. Člen {0}.{1} má anotaci od JsonIncludeAttribute, ale není pro zdrojový generátor viditelný. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf index 7aed1e46895c80..1ab95e16e2fc64 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.de.xlf @@ -32,6 +32,16 @@ Doppelter Typname + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. Der Member "{0}. {1}" wurde mit dem JsonIncludeAttribute versehen, ist jedoch für den Quellgenerator nicht sichtbar. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf index 2fc1195f59670d..f34f668f263ca1 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.es.xlf @@ -32,6 +32,16 @@ Nombre de tipo duplicado. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. El miembro '{0}.{1}' se ha anotado con JsonIncludeAttribute, pero no es visible para el generador de origen. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf index 0e56149e2e5a07..372116c01f1358 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.fr.xlf @@ -32,6 +32,16 @@ Nom de type dupliqué. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. Le membre '{0}.{1}' a été annoté avec JsonIncludeAttribute mais n’est pas visible pour le générateur source. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf index 74f6d8524a3dd0..e122a53a3180eb 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.it.xlf @@ -32,6 +32,16 @@ Nome di tipo duplicato. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. Il membro ' {0}.{1}' è stato annotato con JsonIncludeAttribute ma non è visibile al generatore di origine. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf index 81bc75258af0ed..1591c254a591d4 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ja.xlf @@ -32,6 +32,16 @@ 重複した種類名。 + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. メンバー '{0}.{1}' には、JsonIncludeAttribute で注釈が付けられていますが、ソース ジェネレーターには表示されません。 diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf index bf0e3235fdad0d..e1e889d17aa01c 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ko.xlf @@ -32,6 +32,16 @@ 중복된 형식 이름입니다. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. 멤버 '{0}.{1}'이(가) JsonIncludeAttribute로 주석 처리되었지만 원본 생성기에는 표시되지 않습니다. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf index b38081288b4acf..5bbd998b315fb9 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pl.xlf @@ -32,6 +32,16 @@ Zduplikowana nazwa typu. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. Składowa "{0}. {1}" jest adnotowana za pomocą atrybutu JsonIncludeAttribute, ale nie jest widoczna dla generatora źródła. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf index f9e7dfb256be96..5571d65ad21e67 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.pt-BR.xlf @@ -32,6 +32,16 @@ Nome de tipo duplicado. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. O membro '{0}.{1}' foi anotado com o JsonIncludeAttribute, mas não é visível para o gerador de origem. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf index 82999bc0f676de..ec6e939eeb25ea 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.ru.xlf @@ -32,6 +32,16 @@ Дублирующееся имя типа. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. Элемент "{0}.{1}" аннотирован с использованием класса JsonIncludeAttribute, но генератор исходного кода не обнаруживает этот элемент. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf index 094b385b917b79..7d250a8e54e630 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.tr.xlf @@ -32,6 +32,16 @@ Yinelenen tür adı. + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. '{0}.{1}' üyesine JsonIncludeAttribute notu eklendi ancak bu üye kaynak oluşturucu tarafından görülmüyor. diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf index 400fe650ea5cc9..19aa07edbdce9b 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -32,6 +32,16 @@ 重复的类型名称。 + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. 已使用 JsonIncludeAttribute 注释成员“{0}.{1}”,但对源生成器不可见。 diff --git a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf index f6a48b774b6580..b4681eff17297d 100644 --- a/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/System.Text.Json/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -32,6 +32,16 @@ 重複類型名稱。 + + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + Type '{0}' is annotated with 'JsonDerivedTypeAttribute' which is not supported in 'JsonSourceGenerationMode.Serialization'. + + + + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + 'JsonDerivedTypeAttribute' is not supported in 'JsonSourceGenerationMode.Serialization'. + + The member '{0}.{1}' has been annotated with the JsonIncludeAttribute but is not visible to the source generator. 成員 '{0}.{1}' 已經以 JsonIncludeAttribute 標註,但對來源產生器是不可見的。 diff --git a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs index 39fcd9827b38ed..d3cee9a41c5f91 100644 --- a/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs +++ b/src/libraries/System.Text.Json/gen/TypeGenerationSpec.cs @@ -44,6 +44,7 @@ internal class TypeGenerationSpec public bool ImplementsIJsonOnSerialized { get; private set; } public bool ImplementsIJsonOnSerializing { get; private set; } + public bool IsPolymorphic { get; private set; } public bool IsValueType { get; private set; } public bool CanBeNull { get; private set; } @@ -126,7 +127,8 @@ public void Initialize( bool implementsIJsonOnSerializing, bool hasTypeFactoryConverter, bool canContainNullableReferenceAnnotations, - bool hasPropertyFactoryConverters) + bool hasPropertyFactoryConverters, + bool isPolymorphic) { GenerationMode = generationMode; TypeRef = type.GetCompilableName(); @@ -135,6 +137,7 @@ public void Initialize( ClassType = classType; IsValueType = type.IsValueType; CanBeNull = !IsValueType || nullableUnderlyingTypeMetadata != null; + IsPolymorphic = isPolymorphic; NumberHandling = numberHandling; PropertyGenSpecList = propertyGenSpecList; CtorParamGenSpecArray = ctorParamGenSpecArray; @@ -238,6 +241,11 @@ public bool TryFilterSerializableProps( private bool FastPathIsSupported() { + if (IsPolymorphic) + { + return false; + } + if (ClassType == ClassType.Object) { if (ExtensionDataPropertyTypeSpec != null) diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 83354b3921201f..874d8789c7886d 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -320,6 +320,7 @@ public JsonSerializerOptions(System.Text.Json.JsonSerializerOptions options) { } public System.Text.Json.JsonNamingPolicy? PropertyNamingPolicy { get { throw null; } set { } } public System.Text.Json.JsonCommentHandling ReadCommentHandling { get { throw null; } set { } } public System.Text.Json.Serialization.ReferenceHandler? ReferenceHandler { get { throw null; } set { } } + public System.Collections.Generic.IList PolymorphicTypeConfigurations { get { throw null; } } public System.Text.Json.Serialization.JsonUnknownTypeHandling UnknownTypeHandling { get { throw null; } set { } } public bool WriteIndented { get { throw null; } set { } } public void AddContext() where TContext : System.Text.Json.Serialization.JsonSerializerContext, new() { } @@ -829,6 +830,14 @@ public abstract void Write( System.Text.Json.JsonSerializerOptions options); public virtual void WriteAsPropertyName(System.Text.Json.Utf8JsonWriter writer, T value, System.Text.Json.JsonSerializerOptions options) { } } + [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] + public partial class JsonDerivedTypeAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonDerivedTypeAttribute(System.Type derivedType) { } + public JsonDerivedTypeAttribute(System.Type derivedType, string typeDiscriminatorId) { } + public System.Type DerivedType { get { throw null; } } + public string? TypeDiscriminatorId { get { throw null; } } + } [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false)] public sealed partial class JsonExtensionDataAttribute : System.Text.Json.Serialization.JsonAttribute { @@ -871,6 +880,14 @@ public sealed partial class JsonNumberHandlingAttribute : System.Text.Json.Seria public JsonNumberHandlingAttribute(System.Text.Json.Serialization.JsonNumberHandling handling) { } public System.Text.Json.Serialization.JsonNumberHandling Handling { get { throw null; } } } + [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + public sealed partial class JsonPolymorphicAttribute : System.Text.Json.Serialization.JsonAttribute + { + public JsonPolymorphicAttribute() { } + public string? CustomTypeDiscriminatorPropertyName { get { throw null; } set { } } + public bool IgnoreUnrecognizedTypeDiscriminators { get { throw null; } set { } } + public System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get { throw null; } set { } } + } [System.AttributeUsageAttribute(System.AttributeTargets.Field | System.AttributeTargets.Property, AllowMultiple=false)] public sealed partial class JsonPropertyNameAttribute : System.Text.Json.Serialization.JsonAttribute { @@ -923,11 +940,40 @@ public JsonStringEnumConverter(System.Text.Json.JsonNamingPolicy? namingPolicy = 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, + FallbackToBaseType = 1, + FallbackToNearestAncestor = 2 + } public enum JsonUnknownTypeHandling { JsonElement = 0, JsonNode = 1, } + public partial class JsonPolymorphicTypeConfiguration : System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>, System.Collections.Generic.IEnumerable<(System.Type DerivedType, string? TypeDiscriminatorId)>, System.Collections.IEnumerable + { + public JsonPolymorphicTypeConfiguration(System.Type baseType) { } + public System.Type BaseType { get { throw null; } } + public string? CustomTypeDiscriminatorPropertyName { get { throw null; } set { } } + public bool IgnoreUnrecognizedTypeDiscriminators { get { throw null; } set { } } + int System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>.Count { get { throw null; } } + public System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get { throw null; } set { } } + bool System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>.IsReadOnly { get { throw null; } } + void System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>.Add((System.Type DerivedType, string TypeDiscriminatorId) item) { } + void System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>.Clear() { } + bool System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>.Contains((System.Type DerivedType, string TypeDiscriminatorId) item) { throw null; } + void System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>.CopyTo((System.Type DerivedType, string TypeDiscriminatorId)[] array, int arrayIndex) { } + bool System.Collections.Generic.ICollection<(System.Type DerivedType, string? TypeDiscriminatorId)>.Remove((System.Type DerivedType, string TypeDiscriminatorId) item) { throw null; } + System.Collections.Generic.IEnumerator<(System.Type DerivedType, string? TypeDiscriminatorId)> System.Collections.Generic.IEnumerable<(System.Type DerivedType, string? TypeDiscriminatorId)>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public System.Text.Json.Serialization.JsonPolymorphicTypeConfiguration WithDerivedType(System.Type derivedType, string? typeDiscriminatorId = null) { throw null; } + } + public partial class JsonPolymorphicTypeConfiguration : System.Text.Json.Serialization.JsonPolymorphicTypeConfiguration where TBaseType : class + { + public JsonPolymorphicTypeConfiguration() : base(default(System.Type)) { } + public System.Text.Json.Serialization.JsonPolymorphicTypeConfiguration WithDerivedType(string? typeDiscriminatorId = null) where TDerivedType : TBaseType { throw null; } + } public abstract partial class ReferenceHandler { protected ReferenceHandler() { } diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.csproj b/src/libraries/System.Text.Json/ref/System.Text.Json.csproj index a1c4a020c16194..fba5fe2ab7af9e 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.csproj @@ -15,6 +15,7 @@ + @@ -42,8 +43,9 @@ + - \ No newline at end of file + diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 967d2d087ae00f..f7383fc34ea5c3 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -430,13 +430,13 @@ Invalid leading zero before '{0}'. - Cannot parse a JSON object containing metadata properties like '$id' into an array or immutable collection type. Type '{0}'. + Cannot parse a JSON object containing metadata properties like '$id' or '$type' into an array or immutable collection type. Type '{0}'. The value of the '$id' metadata property '{0}' conflicts with an existing identifier. - The metadata property '$id' must be the first property in the JSON object. + The metadata property '$id' must be the first reference preservation property in the JSON object. Invalid reference to value type '{0}'. @@ -449,23 +449,29 @@ 1. {0} 2. {1} - - Invalid property '{0}' found within a JSON object that must only contain metadata properties and the nested JSON array to be preserved. + + A JSON object containing metadata for a nested array includes a non-metadata property '{0}'. - - One or more metadata properties, such as '$id' and '$values', were not found within a JSON object that must only contain metadata properties and the nested JSON array to be preserved. + + A '$values' metadata property must always be preceded by other metadata properties, such as '$id' or '$type'. A JSON object that contains a '$ref' metadata property must not contain any other properties. - Reference '{0}' not found. + Reference '{0}' was not found. - The '$id' and '$ref' metadata properties must be JSON strings. Current token type is '{0}'. + The '$id', '$ref' or '$type' metadata properties must be JSON strings. Current token type is '{0}'. - Properties that start with '$' are not allowed on preserve mode, either escape the character or turn off preserve references by setting ReferenceHandler to null. + Properties that start with '$' are not allowed in types that support metadata. Either escape the character or disable reference preservation and polymorphic deserialization. + + + The metadata property is either not supported by the type or is not the first property in the deserialized JSON object. + + + Deserialized object contains a duplicate type discriminator metadata property. Members '{0}' and '{1}' on type '{2}' cannot both bind with parameter '{3}' in the deserialization constructor. @@ -611,4 +617,37 @@ 'JsonSerializerContext' '{0}' did not provide constructor parameter metadata for type '{1}'. + + The converter for polymorphic type '{0}' does not support metadata writes or reads. + + + The converter for derived type '{0}' does not support metadata writes or reads. + + + Specified type '{0}' does not support polymorphism. Polymorphic types cannot be structs, sealed types, generic types or System.Object. + + + Specified type '{0}' is not a supported derived type for the polymorphic type '{1}'. Derived types must be assignable to the base type, must not be generic and cannot be abstact classes or interfaces unless 'JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor' is specified. + + + The polymorphic type '{0}' has already specified derived type '{1}'. + + + The polymorphic type '{0}' has already specified a type discriminator '{1}'. + + + The metadata property names '$id', '$ref', and '$values' are reserved and cannot be used as custom type discriminator property names. + + + Polymorphic configuration for type '{0}' should specify at least one derived type. + + + Read unrecognized type discriminator id '{0}'. + + + Runtime type '{0}' is not supported by polymorphic type '{1}'. + + + Runtime type '{0}' has a diamond ambiguity between derived types '{1}' and '{2}' of polymorphic type '{3}'. Consider either removing one of the derived types or removing the 'JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor' setting. + diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index e865ea7500f355..94386db3989278 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -98,10 +98,12 @@ System.Text.Json.Nodes.JsonValue + + @@ -121,12 +123,12 @@ System.Text.Json.Nodes.JsonValue - - + + - + @@ -205,6 +207,7 @@ System.Text.Json.Nodes.JsonValue + @@ -226,12 +229,15 @@ System.Text.Json.Nodes.JsonValue + + + @@ -249,7 +255,9 @@ System.Text.Json.Nodes.JsonValue + + @@ -312,6 +320,7 @@ System.Text.Json.Nodes.JsonValue + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonDerivedTypeAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonDerivedTypeAttribute.cs new file mode 100644 index 00000000000000..0a5128a0e3278b --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonDerivedTypeAttribute.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// When placed on a type declaration, indicates that the specified subtype should be opted into polymorphic serialization. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)] + public class JsonDerivedTypeAttribute : JsonAttribute + { + /// + /// Initializes a new attribute with specified parameters. + /// + /// A derived type that should be supported in polymorphic serialization of the declared based type. + public JsonDerivedTypeAttribute(Type derivedType) + { + DerivedType = derivedType; + } + + /// + /// Initializes a new attribute with specified parameters. + /// + /// A derived type that should be supported in polymorphic serialization of the declared base type. + /// The type discriminator identifier to be used for the serialization of the subtype. + public JsonDerivedTypeAttribute(Type derivedType, string typeDiscriminatorId) + { + DerivedType = derivedType; + TypeDiscriminatorId = typeDiscriminatorId; + } + + /// + /// A derived type that should be supported in polymorphic serialization of the declared base type. + /// + public Type DerivedType { get; } + + /// + /// The type discriminator identifier to be used for the serialization of the subtype. + /// + public string? TypeDiscriminatorId { get; } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPolymorphicAttribute.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPolymorphicAttribute.cs new file mode 100644 index 00000000000000..69f4f8fa965c41 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPolymorphicAttribute.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// When placed on a type, indicates that the type should be serialized polymorphically. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + public sealed class JsonPolymorphicAttribute : JsonAttribute + { + /// + /// Gets or sets a custom type discriminator property name for the polymorhic type. + /// Uses the default '$type' property name if left unset. + /// + public string? CustomTypeDiscriminatorPropertyName { get; set; } + + /// + /// Gets or sets the behavior when serializing an undeclared derived runtime type. + /// + public JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; set; } + + /// + /// When set to , instructs the deserializer to ignore any + /// unrecognized type discriminator id's and reverts to the contract of the base type. + /// Otherwise, it will fail the deserialization. + /// + public bool IgnoreUnrecognizedTypeDiscriminators { get; set; } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConverterList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs similarity index 62% rename from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConverterList.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs index a2f65ff8fe893a..1d916fbba80f7a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConverterList.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ConfigurationList.cs @@ -3,30 +3,33 @@ using System.Collections; using System.Collections.Generic; +using System.Diagnostics; namespace System.Text.Json.Serialization { /// - /// A list of JsonConverters that respects the options class being immuttable once (de)serialization occurs. + /// A list of configuration items that respects the options class being immutable once (de)serialization occurs. /// - internal sealed class ConverterList : IList + internal sealed class ConfigurationList : IList { - private readonly List _list; + private readonly List _list; private readonly JsonSerializerOptions _options; - public ConverterList(JsonSerializerOptions options) + public Action? OnElementAdded { get; set; } + + public ConfigurationList(JsonSerializerOptions options) { _options = options; - _list = new List(); + _list = new List(); } - public ConverterList(JsonSerializerOptions options, ConverterList source) + public ConfigurationList(JsonSerializerOptions options, IList source) { _options = options; - _list = new List(source._list); + _list = new List(source is ConfigurationList cl ? cl._list : source); } - public JsonConverter this[int index] + public TItem this[int index] { get { @@ -41,6 +44,7 @@ public JsonConverter this[int index] _options.VerifyMutable(); _list[index] = value; + OnElementAdded?.Invoke(value); } } @@ -48,10 +52,11 @@ public JsonConverter this[int index] public bool IsReadOnly => false; - public void Add(JsonConverter item!!) + public void Add(TItem item!!) { _options.VerifyMutable(); _list.Add(item); + OnElementAdded?.Invoke(item); } public void Clear() @@ -60,33 +65,34 @@ public void Clear() _list.Clear(); } - public bool Contains(JsonConverter item) + public bool Contains(TItem item) { return _list.Contains(item); } - public void CopyTo(JsonConverter[] array, int arrayIndex) + public void CopyTo(TItem[] array, int arrayIndex) { _list.CopyTo(array, arrayIndex); } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return _list.GetEnumerator(); } - public int IndexOf(JsonConverter item) + public int IndexOf(TItem item) { return _list.IndexOf(item); } - public void Insert(int index, JsonConverter item!!) + public void Insert(int index, TItem item!!) { _options.VerifyMutable(); _list.Insert(index, item); + OnElementAdded?.Invoke(item); } - public bool Remove(JsonConverter item) + public bool Remove(TItem item) { _options.VerifyMutable(); return _list.Remove(item); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs index aff697bdb1c682..90ddfc3dec7f05 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ArrayConverter.cs @@ -53,6 +53,8 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TElement[] array, J return false; } + state.Current.EndCollectionElement(); + if (ShouldFlush(writer, ref state)) { state.Current.EnumeratorIndex = ++index; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs index e53a6fb7a94727..da360f61406660 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs @@ -65,7 +65,7 @@ protected internal override bool OnWriteResume( return false; } - state.Current.EndDictionaryElement(); + state.Current.EndDictionaryEntry(); } while (enumerator.MoveNext()); enumerator.Dispose(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs index d4daf86716664f..cd83ea29259212 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryOfTKeyTValueConverter.cs @@ -80,7 +80,7 @@ protected internal override bool OnWriteResume( return false; } - state.Current.EndDictionaryElement(); + state.Current.EndDictionaryEntry(); } while (enumerator.MoveNext()); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IAsyncEnumerableOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IAsyncEnumerableOfTConverter.cs index 868e94109a8f5c..fc21c0f9c482e1 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IAsyncEnumerableOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IAsyncEnumerableOfTConverter.cs @@ -117,6 +117,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TAsyncEnumerable va return false; } + state.Current.EndCollectionElement(); moveNextTask = enumerator.MoveNextAsync(); } while (moveNextTask.IsCompleted); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs index fb1f553ecac0c8..3d861866599766 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs @@ -89,7 +89,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TDictionar return false; } - state.Current.EndDictionaryElement(); + state.Current.EndDictionaryEntry(); } while (enumerator.MoveNext()); return true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs index 1ae6fd07a21215..05f97509c7ef0c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs @@ -70,6 +70,8 @@ protected override bool OnWriteResume( state.Current.CollectionEnumerator = enumerator; return false; } + + state.Current.EndCollectionElement(); } while (enumerator.MoveNext()); return true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs index f10791029559b7..9a9c62db5708fc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs @@ -49,6 +49,8 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, state.Current.CollectionEnumerator = enumerator; return false; } + + state.Current.EndCollectionElement(); } while (enumerator.MoveNext()); enumerator.Dispose(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs index a77915a6e5467f..7c54b27ad93b5f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IListConverter.cs @@ -61,6 +61,8 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, return false; } + state.Current.EndCollectionElement(); + if (ShouldFlush(writer, ref state)) { state.Current.EnumeratorIndex = ++index; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs index b5aeca4407a6d6..5b32b9cc83a8fe 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonCollectionConverter.cs @@ -72,7 +72,7 @@ internal override bool OnTryRead( { JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!; - if (state.UseFastPath) + if (!state.SupportContinuation && !state.Current.CanContainMetadata) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -121,6 +121,7 @@ internal override bool OnTryRead( else { // Slower path that supports continuation and reading metadata. + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; if (state.Current.ObjectState == StackFrameObjectState.None) { @@ -128,7 +129,7 @@ internal override bool OnTryRead( { state.Current.ObjectState = StackFrameObjectState.ReadMetadata; } - else if (state.CanContainMetadata) + else if (state.Current.CanContainMetadata) { if (reader.TokenType != JsonTokenType.StartObject) { @@ -146,30 +147,42 @@ internal override bool OnTryRead( } // Handle the metadata properties. - if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) + if (state.Current.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) { - if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state)) + if (!JsonSerializer.TryReadMetadata(this, jsonTypeInfo, ref reader, ref state)) { value = default; return false; } + if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref) + { + value = JsonSerializer.ResolveReferenceId(ref state); + return true; + } + state.Current.ObjectState = StackFrameObjectState.ReadMetadata; } + // Dispatch to any polymorphic converters: should always be entered regardless of ObjectState progress + if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Type) && + state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted && + ResolvePolymorphicConverter(jsonTypeInfo, options, ref state) is JsonConverter polymorphicConverter) + { + Debug.Assert(!IsValueType); + bool success = polymorphicConverter.OnTryReadAsObject(ref reader, options, ref state, out object? objectResult); + value = (TCollection)objectResult!; + state.ExitPolymorphicConverter(success); + return success; + } + if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) { - if (state.CanContainMetadata) + if (state.Current.CanContainMetadata) { JsonSerializer.ValidateMetadataForArrayConverter(this, ref reader, ref state); } - if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref) - { - value = JsonSerializer.ResolveReferenceId(ref state); - return true; - } - CreateCollection(ref reader, ref state, options); if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id)) @@ -254,7 +267,7 @@ internal override bool OnTryRead( if (reader.TokenType != JsonTokenType.EndObject) { Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); - ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, typeToConvert, reader); + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyInArrayMetadata(ref state, typeToConvert, reader); } } } @@ -283,17 +296,14 @@ internal override bool OnTryWrite( if (!state.Current.ProcessedStartToken) { state.Current.ProcessedStartToken = true; - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) - { - MetadataPropertyName metadata = JsonSerializer.WriteReferenceForCollection(this, ref state, writer); - Debug.Assert(metadata != MetadataPropertyName.Ref); - state.Current.MetadataPropertyName = metadata; - } - else + + if (state.CurrentContainsMetadata && CanHaveMetadata) { - writer.WriteStartArray(); + state.Current.MetadataPropertyName = JsonSerializer.WriteMetadataForCollection(this, ref state, writer); } + // Writing the start of the array must happen after any metadata + writer.WriteStartArray(); state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; } @@ -305,7 +315,7 @@ internal override bool OnTryWrite( state.Current.ProcessedEndToken = true; writer.WriteEndArray(); - if (state.Current.MetadataPropertyName == MetadataPropertyName.Id) + if (state.Current.MetadataPropertyName != 0) { // Write the EndObject for $values. writer.WriteEndObject(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs index c4ef11398956cc..721f1c64d0faf0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/JsonDictionaryConverter.cs @@ -86,7 +86,7 @@ internal sealed override bool OnTryRead( JsonTypeInfo keyTypeInfo = state.Current.JsonTypeInfo.KeyTypeInfo!; JsonTypeInfo elementTypeInfo = state.Current.JsonTypeInfo.ElementTypeInfo!; - if (state.UseFastPath) + if (!state.SupportContinuation && !state.Current.CanContainMetadata) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -156,6 +156,7 @@ internal sealed override bool OnTryRead( else { // Slower path that supports continuation and reading metadata. + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; if (state.Current.ObjectState == StackFrameObjectState.None) { @@ -168,31 +169,43 @@ internal sealed override bool OnTryRead( } // Handle the metadata properties. - if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) + if (state.Current.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) { - if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state)) + if (!JsonSerializer.TryReadMetadata(this, jsonTypeInfo, ref reader, ref state)) { value = default; return false; } + if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref) + { + value = JsonSerializer.ResolveReferenceId(ref state); + return true; + } + state.Current.ObjectState = StackFrameObjectState.ReadMetadata; } + // Dispatch to any polymorphic converters: should always be entered regardless of ObjectState progress + if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Type) && + state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted && + ResolvePolymorphicConverter(jsonTypeInfo, options, ref state) is JsonConverter polymorphicConverter) + { + Debug.Assert(!IsValueType); + bool success = polymorphicConverter.OnTryReadAsObject(ref reader, options, ref state, out object? objectResult); + value = (TDictionary)objectResult!; + state.ExitPolymorphicConverter(success); + return success; + } + // Create the dictionary. if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) { - if (state.CanContainMetadata) + if (state.Current.CanContainMetadata) { JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state); } - if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref) - { - value = JsonSerializer.ResolveReferenceId(ref state); - return true; - } - CreateCollection(ref reader, ref state); if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Id)) @@ -238,10 +251,10 @@ internal sealed override bool OnTryRead( state.Current.PropertyState = StackFramePropertyState.Name; - if (state.CanContainMetadata) + if (state.Current.CanContainMetadata) { ReadOnlySpan propertyName = reader.GetSpan(); - if (propertyName.Length > 0 && propertyName[0] == '$') + if (JsonSerializer.IsMetadataPropertyName(propertyName, state.Current.BaseJsonTypeInfo.PolymorphicTypeResolver)) { ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); } @@ -326,10 +339,10 @@ internal sealed override bool OnTryWrite( { state.Current.ProcessedStartToken = true; writer.WriteStartObject(); - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) + + if (state.CurrentContainsMetadata && CanHaveMetadata) { - MetadataPropertyName propertyName = JsonSerializer.WriteReferenceForObject(this, ref state, writer); - Debug.Assert(propertyName != MetadataPropertyName.Ref); + JsonSerializer.WriteMetadataForObject(this, ref state, writer); } state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs index 835b827fbc5e5e..35e398fe5869fb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ListOfTConverter.cs @@ -52,6 +52,8 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value, return false; } + state.Current.EndCollectionElement(); + if (ShouldFlush(writer, ref state)) { state.Current.EnumeratorIndex = ++index; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs index 6bfc3e40e437f3..5832092cbb043e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs @@ -64,6 +64,8 @@ protected sealed override bool OnWriteResume(Utf8JsonWriter writer, TCollection state.Current.CollectionEnumerator = enumerator; return false; } + + state.Current.EndCollectionElement(); } while (enumerator.MoveNext()); return true; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs index 2b3a9c0d12fbc7..be523775c64f7c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs @@ -61,6 +61,7 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer if (!state.SupportContinuation && jsonTypeInfo is JsonTypeInfo info && info.SerializeHandler != null && + !state.CurrentContainsMetadata && // Do not use the fast path if state needs to write metadata. info.Options.JsonSerializerContext?.CanUseSerializationLogic == true) { info.SerializeHandler(writer, value); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index bbe8ec34da2067..cc8216363a4da3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -22,7 +22,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, object obj; - if (state.UseFastPath) + if (!state.SupportContinuation && !state.Current.CanContainMetadata) { // Fast path that avoids maintaining state variables and dealing with preserved references. @@ -85,20 +85,38 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, } // Handle the metadata properties. - if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) + if (state.Current.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) { - if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state)) + if (!JsonSerializer.TryReadMetadata(this, jsonTypeInfo, ref reader, ref state)) { value = default; return false; } + if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref) + { + value = JsonSerializer.ResolveReferenceId(ref state); + return true; + } + state.Current.ObjectState = StackFrameObjectState.ReadMetadata; } + // Dispatch to any polymorphic converters: should always be entered regardless of ObjectState progress + if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Type) && + state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted && + ResolvePolymorphicConverter(jsonTypeInfo, options, ref state) is JsonConverter polymorphicConverter) + { + Debug.Assert(!IsValueType); + bool success = polymorphicConverter.OnTryReadAsObject(ref reader, options, ref state, out object? objectResult); + value = (T)objectResult!; + state.ExitPolymorphicConverter(success); + return success; + } + if (state.Current.ObjectState < StackFrameObjectState.CreatedObject) { - if (state.CanContainMetadata) + if (state.Current.CanContainMetadata) { JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state); } @@ -268,10 +286,10 @@ internal sealed override bool OnTryWrite( if (!state.SupportContinuation) { writer.WriteStartObject(); - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) + + if (state.CurrentContainsMetadata && CanHaveMetadata) { - MetadataPropertyName propertyName = JsonSerializer.WriteReferenceForObject(this, ref state, writer); - Debug.Assert(propertyName != MetadataPropertyName.Ref); + JsonSerializer.WriteMetadataForObject(this, ref state, writer); } if (obj is IJsonOnSerializing onSerializing) @@ -318,10 +336,10 @@ internal sealed override bool OnTryWrite( if (!state.Current.ProcessedStartToken) { writer.WriteStartObject(); - if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) + + if (state.CurrentContainsMetadata && CanHaveMetadata) { - MetadataPropertyName propertyName = JsonSerializer.WriteReferenceForObject(this, ref state, writer); - Debug.Assert(propertyName != MetadataPropertyName.Ref); + JsonSerializer.WriteMetadataForObject(this, ref state, writer); } if (obj is IJsonOnSerializing onSerializing) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 812283116c04d4..a3aaf6f298fc97 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -26,7 +26,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo object obj; ArgumentState argumentState = state.Current.CtorArgumentState!; - if (state.UseFastPath) + if (!state.SupportContinuation && !state.Current.CanContainMetadata) { // Fast path that avoids maintaining state variables. @@ -91,6 +91,7 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo else { // Slower path that supports continuation and metadata reads. + JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; if (state.Current.ObjectState == StackFrameObjectState.None) { @@ -103,20 +104,39 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo } // Read any metadata properties. - if (state.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) + if (state.Current.CanContainMetadata && state.Current.ObjectState < StackFrameObjectState.ReadMetadata) { - if (!JsonSerializer.TryReadMetadata(this, ref reader, ref state)) + if (!JsonSerializer.TryReadMetadata(this, jsonTypeInfo, ref reader, ref state)) { value = default; return false; } + if (state.Current.MetadataPropertyNames == MetadataPropertyName.Ref) + { + value = JsonSerializer.ResolveReferenceId(ref state); + return true; + } + state.Current.ObjectState = StackFrameObjectState.ReadMetadata; } + // Dispatch to any polymorphic converters: should always be entered regardless of ObjectState progress + if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Type) && + state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted && + ResolvePolymorphicConverter(jsonTypeInfo, options, ref state) is JsonConverter polymorphicConverter) + { + Debug.Assert(!IsValueType); + bool success = polymorphicConverter.OnTryReadAsObject(ref reader, options, ref state, out object? objectResult); + value = (T)objectResult!; + state.ExitPolymorphicConverter(success); + return success; + } + + // Handle metadata post polymorphic dispatch if (state.Current.ObjectState < StackFrameObjectState.ConstructorArguments) { - if (state.CanContainMetadata) + if (state.Current.CanContainMetadata) { JsonSerializer.ValidateMetadataForObjectConverter(this, ref reader, ref state); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.MetadataHandling.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.MetadataHandling.cs new file mode 100644 index 00000000000000..6206fb3199a19c --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.MetadataHandling.cs @@ -0,0 +1,181 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization +{ + public partial class JsonConverter + { + /// + /// Initializes the state for polymorphic cases and returns the appropriate derived converter. + /// + internal JsonConverter? ResolvePolymorphicConverter(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options, ref ReadStack state) + { + Debug.Assert(!IsValueType); + Debug.Assert(CanHaveMetadata); + Debug.Assert(state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Type)); + Debug.Assert(state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted); + Debug.Assert(jsonTypeInfo.PolymorphicTypeResolver?.UsesTypeDiscriminators == true); + + JsonConverter? polymorphicConverter = null; + + switch (state.Current.PolymorphicSerializationState) + { + case PolymorphicSerializationState.None: + Debug.Assert(!state.IsContinuation); + Debug.Assert(state.PolymorphicTypeDiscriminator != null); + + PolymorphicTypeResolver resolver = jsonTypeInfo.PolymorphicTypeResolver; + if (resolver.TryGetDerivedJsonTypeInfo(state.PolymorphicTypeDiscriminator, out JsonTypeInfo? resolvedType)) + { + Debug.Assert(TypeToConvert.IsAssignableFrom(resolvedType.Type)); + + polymorphicConverter = state.InitializePolymorphicReEntry(resolvedType); + if (!polymorphicConverter.CanHaveMetadata) + { + ThrowHelper.ThrowNotSupportedException_DerivedConverterDoesNotSupportMetadata(resolvedType.Type); + } + } + else + { + state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound; + } + + state.PolymorphicTypeDiscriminator = null; + break; + + case PolymorphicSerializationState.PolymorphicReEntrySuspended: + polymorphicConverter = state.ResumePolymorphicReEntry(); + Debug.Assert(TypeToConvert.IsAssignableFrom(polymorphicConverter.TypeToConvert)); + break; + + case PolymorphicSerializationState.PolymorphicReEntryNotFound: + Debug.Assert(state.Current.PolymorphicJsonTypeInfo is null); + break; + + default: + Debug.Fail("Unexpected PolymorphicSerializationState."); + break; + } + + return polymorphicConverter; + } + + /// + /// Initializes the state for polymorphic cases and returns the appropriate derived converter. + /// + internal JsonConverter? ResolvePolymorphicConverter(object value, JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options, ref WriteStack state) + { + Debug.Assert(!IsValueType); + Debug.Assert(value != null && TypeToConvert.IsAssignableFrom(value.GetType())); + Debug.Assert(CanBePolymorphic || jsonTypeInfo.PolymorphicTypeResolver != null); + Debug.Assert(state.PolymorphicTypeDiscriminator is null); + + JsonConverter? polymorphicConverter = null; + + switch (state.Current.PolymorphicSerializationState) + { + case PolymorphicSerializationState.None: + Debug.Assert(!state.IsContinuation); + + Type runtimeType = value.GetType(); + + if (jsonTypeInfo.PolymorphicTypeResolver is PolymorphicTypeResolver resolver) + { + Debug.Assert(CanHaveMetadata); + + if (resolver.TryGetDerivedJsonTypeInfo(runtimeType, out JsonTypeInfo? derivedJsonTypeInfo, out string? typeDiscriminatorId)) + { + polymorphicConverter = state.Current.InitializePolymorphicReEntry(derivedJsonTypeInfo); + + if (typeDiscriminatorId is not null) + { + if (!polymorphicConverter.CanHaveMetadata) + { + ThrowHelper.ThrowNotSupportedException_DerivedConverterDoesNotSupportMetadata(derivedJsonTypeInfo.Type); + } + + state.PolymorphicTypeDiscriminator = typeDiscriminatorId; + } + } + else + { + state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound; + } + } + else + { + Debug.Assert(CanBePolymorphic); + + if (runtimeType != TypeToConvert) + { + polymorphicConverter = state.Current.InitializePolymorphicReEntry(runtimeType, options); + } + else + { + state.Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryNotFound; + } + } + break; + + case PolymorphicSerializationState.PolymorphicReEntrySuspended: + Debug.Assert(state.IsContinuation); + polymorphicConverter = state.Current.ResumePolymorphicReEntry(); + Debug.Assert(TypeToConvert.IsAssignableFrom(polymorphicConverter.TypeToConvert)); + break; + + case PolymorphicSerializationState.PolymorphicReEntryNotFound: + Debug.Assert(state.IsContinuation); + break; + + default: + Debug.Fail("Unexpected PolymorphicSerializationState."); + break; + } + + return polymorphicConverter; + } + + internal bool TryHandleSerializedObjectReference(Utf8JsonWriter writer, object value, JsonSerializerOptions options, JsonConverter? polymorphicConverter, ref WriteStack state) + { + Debug.Assert(!IsValueType); + Debug.Assert(!state.IsContinuation); + Debug.Assert(value != null); + + switch (options.ReferenceHandlingStrategy) + { + case ReferenceHandlingStrategy.IgnoreCycles: + ReferenceResolver resolver = state.ReferenceResolver; + if (resolver.ContainsReferenceForCycleDetection(value)) + { + writer.WriteNullValue(); + return true; + } + + resolver.PushReferenceForCycleDetection(value); + // WriteStack reuses root-level stackframes for its children as a performance optimization; + // we want to avoid writing any data for the root-level object to avoid corrupting the stack. + // This is fine since popping the root object at the end of serialization is not essential. + state.Current.IsPushedReferenceForCycleDetection = state.CurrentDepth > 0; + break; + + case ReferenceHandlingStrategy.Preserve: + bool canHaveIdMetata = polymorphicConverter?.CanHaveMetadata ?? CanHaveMetadata; + if (canHaveIdMetata && JsonSerializer.TryGetReferenceForValue(value, ref state, writer)) + { + // We found a repeating reference and wrote the relevant metadata; serialization complete. + return true; + } + break; + + default: + Debug.Fail("Unexpected ReferenceHandlingStrategy."); + break; + } + + return false; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index a02ca760afdc85..1f2ea53ee88b33 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -29,7 +29,7 @@ internal JsonConverter() { } internal bool CanUseDirectReadOrWrite { get; set; } /// - /// Can the converter have $id metadata. + /// The converter supports writing and reading metadata. /// internal virtual bool CanHaveMetadata => false; @@ -103,6 +103,7 @@ internal static bool ShouldFlush(Utf8JsonWriter writer, ref WriteStack state) // This is used internally to quickly determine the type being converted for JsonConverter. internal abstract Type TypeToConvert { get; } + internal abstract bool OnTryReadAsObject(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, out object? value); internal abstract bool TryReadAsObject(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, out object? value); internal abstract bool TryWriteAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, ref WriteStack state); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs index 014acac841a771..103425277d1c7c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterFactory.cs @@ -78,6 +78,17 @@ internal sealed override object ReadCoreAsObject( throw new InvalidOperationException(); } + internal sealed override bool OnTryReadAsObject( + ref Utf8JsonReader reader, + JsonSerializerOptions options, + ref ReadStack state, + out object? value) + { + Debug.Fail("We should never get here."); + + throw new InvalidOperationException(); + } + internal sealed override bool TryReadAsObject( ref Utf8JsonReader reader, JsonSerializerOptions options, diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index f7f6d2afe3c9f8..6154983c61d2d5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -246,6 +246,13 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali return success; } + internal override sealed bool OnTryReadAsObject(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, out object? value) + { + bool success = OnTryRead(ref reader, TypeToConvert, options, ref state, out T? typedValue); + value = typedValue; + return success; + } + internal override sealed bool TryReadAsObject(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state, out object? value) { bool success = TryRead(ref reader, TypeToConvert, options, ref state, out T? typedValue); @@ -310,52 +317,23 @@ value is not null && // handled by a polymorphic converter for a base type. state.Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted) { - JsonConverter? polymorphicConverter = CanBePolymorphic ? - state.Current.ResolvePolymorphicConverter(value, TypeToConvert, options) : - null; + JsonTypeInfo jsonTypeInfo = state.PeekNestedJsonTypeInfo(); + Debug.Assert(jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase.TypeToConvert == TypeToConvert); - Debug.Assert(polymorphicConverter is null || state.CurrentDepth > 0, - "root-level polymorphic converters should not be handled here."); + bool canBePolymorphic = CanBePolymorphic || jsonTypeInfo.PolymorphicTypeResolver is not null; + JsonConverter? polymorphicConverter = canBePolymorphic ? + ResolvePolymorphicConverter(value, jsonTypeInfo, options, ref state) : + null; - if (!isContinuation) + if (!isContinuation && options.ReferenceHandlingStrategy != ReferenceHandlingStrategy.None && + TryHandleSerializedObjectReference(writer, value, options, polymorphicConverter, ref state)) { - switch (options.ReferenceHandlingStrategy) - { - case ReferenceHandlingStrategy.IgnoreCycles: - ReferenceResolver resolver = state.ReferenceResolver; - if (resolver.ContainsReferenceForCycleDetection(value)) - { - writer.WriteNullValue(); - return true; - } - - resolver.PushReferenceForCycleDetection(value); - // WriteStack reuses root-level stackframes for its children as a performance optimization; - // we want to avoid writing any data for the root-level object to avoid corrupting the stack. - // This is fine since popping the root object at the end of serialization is not essential. - state.Current.IsPushedReferenceForCycleDetection = state.CurrentDepth > 0; - break; - - case ReferenceHandlingStrategy.Preserve: - bool canHaveMetadata = polymorphicConverter?.CanHaveMetadata ?? CanHaveMetadata; - if (canHaveMetadata && JsonSerializer.TryGetReferenceForValue(value, ref state, writer)) - { - // We found a repeating reference and wrote the relevant metadata; serialization complete. - return true; - } - break; - - default: - Debug.Assert(options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.None); - break; - } + // The reference handler wrote reference metadata, serialization complete. + return true; } if (polymorphicConverter is not null) { - Debug.Assert(!polymorphicConverter.CanBePolymorphic, "Only ObjectConverter supports polymorphism."); - - state.Current.EnterPolymorphicConverter(); success = polymorphicConverter.TryWriteAsObject(writer, value, options, ref state); state.Current.ExitPolymorphicConverter(success); @@ -458,7 +436,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json return success; } - internal sealed override Type TypeToConvert => typeof(T); + internal sealed override Type TypeToConvert { get; } = typeof(T); internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, bool isValueConverter, ref Utf8JsonReader reader) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPolymorphicTypeConfiguration.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPolymorphicTypeConfiguration.cs new file mode 100644 index 00000000000000..5d0330c1783ac1 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPolymorphicTypeConfiguration.cs @@ -0,0 +1,191 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Serialization +{ + /// + /// Defines polymorphic configuration for a specified base type. + /// + public class JsonPolymorphicTypeConfiguration : IJsonPolymorphicTypeConfiguration, ICollection<(Type DerivedType, string? TypeDiscriminatorId)> + { + private readonly List<(Type DerivedType, string? TypeDiscriminatorId)> _derivedTypes = new(); + private string? _customTypeDiscriminatorPropertyName; + private JsonUnknownDerivedTypeHandling _unknownDerivedTypeHandling; + private bool _ignoreUnrecognizedTypeDiscriminators; + + /// + /// Creates a new polymorphic configuration instance for a given base type. + /// + /// The base type for which to configure polymorphic serialization. + public JsonPolymorphicTypeConfiguration(Type baseType) + { + if (baseType is null) + { + throw new ArgumentNullException(nameof(baseType)); + } + + if (!PolymorphicTypeResolver.IsSupportedPolymorphicBaseType(baseType)) + { + throw new ArgumentException(SR.Format(SR.Polymorphism_TypeDoesNotSupportPolymorphism, baseType), nameof(baseType)); + } + + BaseType = baseType; + } + + /// + /// Gets the base type for which polymorphic serialization is being configured. + /// + public Type BaseType { get; } + + /// + /// Gets or sets the behavior when serializing an undeclared derived runtime type. + /// + public JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling + { + get => _unknownDerivedTypeHandling; + set + { + VerifyMutable(); + _unknownDerivedTypeHandling = value; + } + } + + /// + /// When set to , instructs the serializer to ignore any + /// unrecognized type discriminator id's and reverts to the contract of the base type. + /// Otherwise, it will fail the deserialization. + /// + public bool IgnoreUnrecognizedTypeDiscriminators + { + get => _ignoreUnrecognizedTypeDiscriminators; + set + { + VerifyMutable(); + _ignoreUnrecognizedTypeDiscriminators = value; + } + } + + /// + /// Gets or sets a custom type discriminator property name for the polymorhic type. + /// Uses the default '$type' property name if left unset. + /// + public string? CustomTypeDiscriminatorPropertyName + { + get => _customTypeDiscriminatorPropertyName; + set + { + VerifyMutable(); + _customTypeDiscriminatorPropertyName = value; + } + } + + /// + /// Opts in polymorphic serialization for the specified derived type. + /// + /// The derived type for which to enable polymorphism. + /// The type discriminator id to use for the specified derived type. + /// The same instance after it has been updated. + public JsonPolymorphicTypeConfiguration WithDerivedType(Type derivedType, string? typeDiscriminatorId = null) + { + VerifyMutable(); + + if (derivedType is null) + { + throw new ArgumentNullException(nameof(derivedType)); + } + + if (!PolymorphicTypeResolver.IsSupportedDerivedType(BaseType, derivedType)) + { + throw new ArgumentException(SR.Format(SR.Polymorphism_DerivedTypeIsNotSupported, derivedType, BaseType), nameof(derivedType)); + } + + // Perform a linear traversal to determine any duplicate derived types or discriminator Id's + // The assumption is that each type maintains a small number of subtypes so this is preferable + // to maintaing hashtables to existing entries. + foreach ((Type DerivedType, string? TypeDiscriminatorId) entry in _derivedTypes) + { + if (entry.DerivedType == derivedType) + { + throw new ArgumentException(SR.Format(SR.Polymorphism_DerivedTypeIsAlreadySpecified, BaseType, derivedType), nameof(derivedType)); + } + + if (typeDiscriminatorId != null && entry.TypeDiscriminatorId == typeDiscriminatorId) + { + throw new ArgumentException(SR.Format(SR.Polymorphism_TypeDicriminatorIdIsAlreadySpecified, BaseType, typeDiscriminatorId), nameof(typeDiscriminatorId)); + } + } + + // Validation complete; update the configuration state. + _derivedTypes.Add((derivedType, typeDiscriminatorId)); + return this; + } + + IEnumerable<(Type DerivedType, string? TypeDiscriminatorId)> IJsonPolymorphicTypeConfiguration.GetSupportedDerivedTypes() + { + foreach ((Type, string?) entry in _derivedTypes) + { + yield return entry; + } + } + + internal bool IsAssignedToOptionsInstance { get; set; } + + private void VerifyMutable() + { + if (IsAssignedToOptionsInstance) + { + ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(context: null); + } + } + + bool ICollection<(Type DerivedType, string? TypeDiscriminatorId)>.Contains((Type DerivedType, string? TypeDiscriminatorId) item) => _derivedTypes.Contains(item); + void ICollection<(Type DerivedType, string? TypeDiscriminatorId)>.CopyTo((Type DerivedType, string? TypeDiscriminatorId)[] array, int arrayIndex) => _derivedTypes.CopyTo(array, arrayIndex); + bool ICollection<(Type DerivedType, string? TypeDiscriminatorId)>.Remove((Type DerivedType, string? TypeDiscriminatorId) item) + { + VerifyMutable(); + return _derivedTypes.Remove(item); + } + + bool ICollection<(Type DerivedType, string? TypeDiscriminatorId)>.IsReadOnly => IsAssignedToOptionsInstance; + int ICollection<(Type DerivedType, string? TypeDiscriminatorId)>.Count => _derivedTypes.Count; + void ICollection<(Type DerivedType, string? TypeDiscriminatorId)>.Add((Type DerivedType, string? TypeDiscriminatorId) item) => WithDerivedType(item.DerivedType, item.TypeDiscriminatorId); + void ICollection<(Type DerivedType, string? TypeDiscriminatorId)>.Clear() + { + VerifyMutable(); + _derivedTypes.Clear(); + } + + IEnumerator<(Type DerivedType, string? TypeDiscriminatorId)> IEnumerable<(Type DerivedType, string? TypeDiscriminatorId)>.GetEnumerator() => _derivedTypes.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _derivedTypes.GetEnumerator(); + } + + /// + /// Defines polymorphic type configuration for a given type. + /// + /// The type for which polymorphic configuration is provided. + public class JsonPolymorphicTypeConfiguration : JsonPolymorphicTypeConfiguration where TBaseType : class + { + /// + /// Creates a new polymorphic configuration instance for a given base type. + /// + public JsonPolymorphicTypeConfiguration() : base(typeof(TBaseType)) + { + } + + /// + /// Associates specified derived type with supplied string identifier. + /// + /// The derived type with which to associate a type identifier. + /// The type identifier to use for the specified derived type. + /// The same instance after it has been updated. + public JsonPolymorphicTypeConfiguration WithDerivedType(string? typeDiscriminatorId = null) where TDerivedType : TBaseType + { + WithDerivedType(typeof(TDerivedType), typeDiscriminatorId); + return this; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs index b262cddd2a1e08..37ed101dfc9608 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandleMetadata.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace System.Text.Json { @@ -16,13 +17,16 @@ internal static readonly byte[] s_idPropertyName internal static readonly byte[] s_refPropertyName = new byte[] { (byte)'$', (byte)'r', (byte)'e', (byte)'f' }; + internal static readonly byte[] s_typePropertyName + = new byte[] { (byte)'$', (byte)'t', (byte)'y', (byte)'p', (byte)'e' }; + internal static readonly byte[] s_valuesPropertyName = new byte[] { (byte)'$', (byte)'v', (byte)'a', (byte)'l', (byte)'u', (byte)'e', (byte)'s' }; - internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader reader, ref ReadStack state) + internal static bool TryReadMetadata(JsonConverter converter, JsonTypeInfo jsonTypeInfo, ref Utf8JsonReader reader, ref ReadStack state) { Debug.Assert(state.Current.ObjectState == StackFrameObjectState.StartToken); - Debug.Assert(state.CanContainMetadata); + Debug.Assert(state.Current.CanContainMetadata); while (true) { @@ -55,11 +59,16 @@ internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader } ReadOnlySpan propertyName = reader.GetSpan(); - switch (state.Current.LatestMetadataPropertyName = GetMetadataPropertyName(propertyName)) + switch (state.Current.LatestMetadataPropertyName = GetMetadataPropertyName(propertyName, jsonTypeInfo.PolymorphicTypeResolver)) { case MetadataPropertyName.Id: state.Current.JsonPropertyName = s_idPropertyName; + if (state.ReferenceResolver is null) + { + // Found an $id property in a type that doesn't support reference preservation + ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(propertyName, ref state); + } if ((state.Current.MetadataPropertyNames & (MetadataPropertyName.Id | MetadataPropertyName.Ref)) != 0) { // No $id or $ref properties should precede $id properties. @@ -76,6 +85,11 @@ internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader case MetadataPropertyName.Ref: state.Current.JsonPropertyName = s_refPropertyName; + if (state.ReferenceResolver is null) + { + // Found a $ref property in a type that doesn't support reference preservation + ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(propertyName, ref state); + } if (converter.IsValueType) { // Should not be permitted if the converter is a struct. @@ -89,13 +103,28 @@ internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader break; + case MetadataPropertyName.Type: + state.Current.JsonPropertyName = jsonTypeInfo.PolymorphicTypeResolver?.CustomTypeDiscriminatorPropertyNameUtf8 ?? s_typePropertyName; + + if (jsonTypeInfo.PolymorphicTypeResolver is null) + { + // Found a $type property in a type that doesn't support polymorphism + ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(propertyName, ref state); + } + if (state.PolymorphicTypeDiscriminator != null) + { + ThrowHelper.ThrowJsonException_MetadataDuplicateTypeProperty(); + } + + break; + case MetadataPropertyName.Values: state.Current.JsonPropertyName = s_valuesPropertyName; if (state.Current.MetadataPropertyNames == MetadataPropertyName.None) { // Cannot have a $values property unless there are preceding metadata properties. - ThrowHelper.ThrowJsonException_MetadataMissingIdBeforeValues(ref state, propertyName); + ThrowHelper.ThrowJsonException_MetadataStandaloneValuesProperty(ref state, propertyName); } break; @@ -153,6 +182,16 @@ internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader state.ReferenceId = reader.GetString(); break; + case MetadataPropertyName.Type: + if (reader.TokenType != JsonTokenType.String) + { + ThrowHelper.ThrowJsonException_MetadataValueWasNotString(reader.TokenType); + } + + Debug.Assert(state.PolymorphicTypeDiscriminator == null); + state.PolymorphicTypeDiscriminator = reader.GetString(); + break; + case MetadataPropertyName.Values: if (reader.TokenType != JsonTokenType.StartArray) @@ -174,7 +213,15 @@ internal static bool TryReadMetadata(JsonConverter converter, ref Utf8JsonReader state.Current.JsonPropertyName = null; } } - internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan propertyName) + + internal static bool IsMetadataPropertyName(ReadOnlySpan propertyName, PolymorphicTypeResolver? resolver) + { + return + (propertyName.Length > 0 && propertyName[0] == '$') || + (resolver?.CustomTypeDiscriminatorPropertyNameUtf8?.AsSpan().SequenceEqual(propertyName) == true); + } + + internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan propertyName, PolymorphicTypeResolver? resolver) { if (propertyName.Length > 0 && propertyName[0] == '$') { @@ -197,6 +244,16 @@ internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan } break; + case 5 when resolver?.CustomTypeDiscriminatorPropertyNameUtf8 is null: + if (propertyName[1] == 't' && + propertyName[2] == 'y' && + propertyName[3] == 'p' && + propertyName[4] == 'e') + { + return MetadataPropertyName.Type; + } + break; + case 7: if (propertyName[1] == 'v' && propertyName[2] == 'a' && @@ -211,6 +268,12 @@ internal static MetadataPropertyName GetMetadataPropertyName(ReadOnlySpan } } + if (resolver?.CustomTypeDiscriminatorPropertyNameUtf8 is byte[] customTypeDiscriminator && + propertyName.SequenceEqual(customTypeDiscriminator)) + { + return MetadataPropertyName.Type; + } + return MetadataPropertyName.None; } @@ -361,7 +424,7 @@ internal static void ValidateMetadataForObjectConverter(JsonConverter converter, if (state.Current.MetadataPropertyNames.HasFlag(MetadataPropertyName.Values)) { // Object converters do not support $values metadata. - ThrowHelper.ThrowJsonException_MetadataInvalidPropertyWithLeadingDollarSign(s_valuesPropertyName, ref state, reader); + ThrowHelper.ThrowJsonException_MetadataUnexpectedProperty(s_valuesPropertyName, ref state); } } @@ -384,7 +447,7 @@ internal static void ValidateMetadataForArrayConverter(JsonConverter converter, default: Debug.Assert(reader.TokenType == JsonTokenType.PropertyName); // Do not tolerate non-metadata properties in collection converters. - ThrowHelper.ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref state, converter.TypeToConvert, reader); + ThrowHelper.ThrowJsonException_MetadataInvalidPropertyInArrayMetadata(ref state, converter.TypeToConvert, reader); break; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index 0e55755c92c1aa..d5ea1545b42ea6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -96,9 +96,9 @@ internal static ReadOnlySpan GetPropertyName( unescapedPropertyName = propertyName; } - if (state.CanContainMetadata) + if (state.Current.CanContainMetadata) { - if (propertyName.Length > 0 && propertyName[0] == '$') + if (IsMetadataPropertyName(propertyName, state.Current.BaseJsonTypeInfo.PolymorphicTypeResolver)) { ThrowHelper.ThrowUnexpectedMetadataException(propertyName, ref reader, ref state); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs index f215fc1b95f0bb..22052cbb409709 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleMetadata.cs @@ -11,42 +11,55 @@ public static partial class JsonSerializer // Pre-encoded metadata properties. internal static readonly JsonEncodedText s_metadataId = JsonEncodedText.Encode("$id", encoder: null); internal static readonly JsonEncodedText s_metadataRef = JsonEncodedText.Encode("$ref", encoder: null); + internal static readonly JsonEncodedText s_metadataType = JsonEncodedText.Encode("$type", encoder: null); internal static readonly JsonEncodedText s_metadataValues = JsonEncodedText.Encode("$values", encoder: null); - internal static MetadataPropertyName WriteReferenceForObject( + internal static MetadataPropertyName WriteMetadataForObject( JsonConverter jsonConverter, ref WriteStack state, Utf8JsonWriter writer) { + Debug.Assert(jsonConverter.CanHaveMetadata); + Debug.Assert(!state.IsContinuation); + Debug.Assert(state.CurrentContainsMetadata); + + MetadataPropertyName writtenMetadata = MetadataPropertyName.None; + if (state.NewReferenceId != null) { - Debug.Assert(jsonConverter.CanHaveMetadata); writer.WriteString(s_metadataId, state.NewReferenceId); + writtenMetadata |= MetadataPropertyName.Id; state.NewReferenceId = null; - return MetadataPropertyName.Id; } - return MetadataPropertyName.None; + if (state.PolymorphicTypeDiscriminator is string typeDiscriminatorId) + { + Debug.Assert(state.Parent.JsonPropertyInfo!.JsonTypeInfo.PolymorphicTypeResolver != null); + + JsonEncodedText propertyName = + state.Parent.JsonPropertyInfo.JsonTypeInfo.PolymorphicTypeResolver.CustomTypeDiscriminatorPropertyNameJsonEncoded is JsonEncodedText customPropertyName + ? customPropertyName + : s_metadataType; + + writer.WriteString(propertyName, typeDiscriminatorId); + writtenMetadata |= MetadataPropertyName.Type; + state.PolymorphicTypeDiscriminator = null; + } + + Debug.Assert(writtenMetadata != MetadataPropertyName.None); + return writtenMetadata; } - internal static MetadataPropertyName WriteReferenceForCollection( + internal static MetadataPropertyName WriteMetadataForCollection( JsonConverter jsonConverter, ref WriteStack state, Utf8JsonWriter writer) { - if (state.NewReferenceId != null) - { - Debug.Assert(jsonConverter.CanHaveMetadata); - writer.WriteStartObject(); - writer.WriteString(s_metadataId, state.NewReferenceId); - writer.WriteStartArray(s_metadataValues); - state.NewReferenceId = null; - return MetadataPropertyName.Id; - } - - // If the jsonConverter supports immutable enumerables or value type collections, don't write any metadata - writer.WriteStartArray(); - return MetadataPropertyName.None; + // For collections with metadata, we nest the array payload within a JSON object. + writer.WriteStartObject(); + MetadataPropertyName writtenMetadata = WriteMetadataForObject(jsonConverter, ref state, writer); + writer.WritePropertyName(s_metadataValues); // property name containing nested array values. + return writtenMetadata; } /// @@ -65,6 +78,8 @@ internal static bool TryGetReferenceForValue(object currentValue, ref WriteStack writer.WriteStartObject(); writer.WriteString(s_metadataRef, referenceId); writer.WriteEndObject(); + + state.PolymorphicTypeDiscriminator = null; // clear out any polymorphism state. } else { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index fd62229d44f175..482cdc0466fb0b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -288,9 +288,12 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive && left._writeIndented == right._writeIndented && left._serializerContext == right._serializerContext && - CompareConverters(left._converters, right._converters); + CompareLists(left._converters, right._converters) && +#pragma warning disable CA2252 // This API requires opting into preview features + CompareLists(left._polymorphicTypeConfigurations, right._polymorphicTypeConfigurations); +#pragma warning restore CA2252 // This API requires opting into preview features - static bool CompareConverters(ConverterList left, ConverterList right) + static bool CompareLists(ConfigurationList left, ConfigurationList right) { int n; if ((n = left.Count) != right.Count) @@ -300,7 +303,7 @@ static bool CompareConverters(ConverterList left, ConverterList right) for (int i = 0; i < n; i++) { - if (left[i] != right[i]) + if (!left[i]!.Equals(right[i])) { return false; } @@ -332,10 +335,17 @@ public int GetHashCode(JsonSerializerOptions options) hc.Add(options._propertyNameCaseInsensitive); hc.Add(options._writeIndented); hc.Add(options._serializerContext); + GetHashCode(ref hc, options._converters); +#pragma warning disable CA2252 // This API requires opting into preview features + GetHashCode(ref hc, options._polymorphicTypeConfigurations); +#pragma warning restore CA2252 // This API requires opting into preview features - for (int i = 0; i < options._converters.Count; i++) + static void GetHashCode(ref HashCode hc, ConfigurationList list) { - hc.Add(options._converters[i]); + for (int i = 0; i < list.Count; i++) + { + hc.Add(list[i]); + } } return hc.ToHashCode(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 2f12221b70bdb1..f79659f8d91f85 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -140,6 +140,14 @@ void Add(JsonConverter converter) => /// public IList Converters => _converters; + /// + /// The list of custom polymorphic type configurations. + /// + /// + /// Once serialization or deserialization occurs, the list cannot be modified. + /// + public IList PolymorphicTypeConfigurations => _polymorphicTypeConfigurations; + internal JsonConverter GetConverterFromMember(Type? parentClassType, Type propertyType, MemberInfo? memberInfo) { JsonConverter converter = null!; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs index 6cb3cdb0c825fa..df114e685663c4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs @@ -42,7 +42,10 @@ public sealed partial class JsonSerializerOptions private JsonCommentHandling _readCommentHandling; private ReferenceHandler? _referenceHandler; private JavaScriptEncoder? _encoder; - private ConverterList _converters; + private ConfigurationList _converters; +#pragma warning disable CA2252 // This API requires opting into preview features + private ConfigurationList _polymorphicTypeConfigurations; +#pragma warning restore CA2252 // This API requires opting into preview features private JsonIgnoreCondition _defaultIgnoreCondition; private JsonNumberHandling _numberHandling; private JsonUnknownTypeHandling _unknownTypeHandling; @@ -62,7 +65,15 @@ public sealed partial class JsonSerializerOptions /// public JsonSerializerOptions() { - _converters = new ConverterList(this); + _converters = new ConfigurationList(this); + +#pragma warning disable CA2252 // This API requires opting into preview features + _polymorphicTypeConfigurations = new ConfigurationList(this) + { + OnElementAdded = static config => { config.IsAssignedToOptionsInstance = true; } + }; +#pragma warning restore CA2252 // This API requires opting into preview features + TrackOptionsInstance(this); } @@ -80,6 +91,10 @@ public JsonSerializerOptions(JsonSerializerOptions options!!) _jsonPropertyNamingPolicy = options._jsonPropertyNamingPolicy; _readCommentHandling = options._readCommentHandling; _referenceHandler = options._referenceHandler; + _converters = new ConfigurationList(this, options._converters); +#pragma warning disable CA2252 // This API requires opting into preview features + _polymorphicTypeConfigurations = new ConfigurationList(this, options._polymorphicTypeConfigurations); +#pragma warning restore CA2252 // This API requires opting into preview features _encoder = options._encoder; _defaultIgnoreCondition = options._defaultIgnoreCondition; _numberHandling = options._numberHandling; @@ -95,15 +110,12 @@ public JsonSerializerOptions(JsonSerializerOptions options!!) _propertyNameCaseInsensitive = options._propertyNameCaseInsensitive; _writeIndented = options._writeIndented; - _converters = new ConverterList(this, options._converters); EffectiveMaxDepth = options.EffectiveMaxDepth; ReferenceHandlingStrategy = options.ReferenceHandlingStrategy; - // _classes is not copied as sharing the JsonTypeInfo and JsonPropertyInfo caches can result in + // _cachingContext is not copied as sharing the JsonTypeInfo and JsonPropertyInfo caches can result in // unnecessary references to type metadata, potentially hindering garbage collection on the source options. - // _haveTypesBeenCreated is not copied; it's okay to make changes to this options instance as (de)serialization has not occurred. - TrackOptionsInstance(this); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownDerivedTypeHandling.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownDerivedTypeHandling.cs new file mode 100644 index 00000000000000..05fa3ca8f1aa0d --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonUnknownDerivedTypeHandling.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization +{ + /// + /// Defines how objects of a derived runtime type that has not been explicitly declared for polymorphic serialization should be handled. + /// + public enum JsonUnknownDerivedTypeHandling + { + /// + /// An object of undeclared runtime type will fail polymorphic serialization. + /// + FailSerialization = 0, + /// + /// An object of undeclared runtime type will fall back to the serialization contract of the base type. + /// + FallbackToBaseType = 1, + /// + /// An object of undeclared runtime type will revert to the serialization contract of the nearest declared ancestor type. + /// Certain interface hierarchies are not supported due to diamond ambiguity constraints. + /// + FallbackToNearestAncestor = 2 + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/AttributePolymorphicTypeConfiguration.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/AttributePolymorphicTypeConfiguration.cs new file mode 100644 index 00000000000000..2b835b53557b5e --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/AttributePolymorphicTypeConfiguration.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Reflection; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Maps attribute-based polymorphism configuration to IJsonPolymorphicTypeConfiguration + /// + internal class AttributePolymorphicTypeConfiguration : IJsonPolymorphicTypeConfiguration + { +#pragma warning disable CA2252 // This API requires opting into preview features + private readonly JsonPolymorphicAttribute? _polymorphicTypeAttribute; + private readonly IEnumerable _derivedTypeAttributes; + + private AttributePolymorphicTypeConfiguration(Type baseType, JsonPolymorphicAttribute? polymorphicTypeAttribute, IEnumerable derivedTypeAttributes) + { + BaseType = baseType; + _polymorphicTypeAttribute = polymorphicTypeAttribute; + _derivedTypeAttributes = derivedTypeAttributes; + } + + public static AttributePolymorphicTypeConfiguration? Create(Type baseType) + { + JsonPolymorphicAttribute? polymorphicTypeAttribute = baseType.GetCustomAttribute(inherit: false); + IEnumerable derivedTypeAttributes = baseType.GetCustomAttributes(inherit: false); + + if (polymorphicTypeAttribute is null && IsEmpty(derivedTypeAttributes)) + { + return null; + } + + return new AttributePolymorphicTypeConfiguration(baseType, polymorphicTypeAttribute, derivedTypeAttributes); + + static bool IsEmpty(IEnumerable source) + { + using IEnumerator enumerator = source.GetEnumerator(); + return !enumerator.MoveNext(); + } + } + + public Type BaseType { get; } + + public string? CustomTypeDiscriminatorPropertyName => _polymorphicTypeAttribute?.CustomTypeDiscriminatorPropertyName; + + public JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling => _polymorphicTypeAttribute?.UnknownDerivedTypeHandling ?? default; + + public bool IgnoreUnrecognizedTypeDiscriminators => _polymorphicTypeAttribute?.IgnoreUnrecognizedTypeDiscriminators ?? false; + + public IEnumerable<(Type DerivedType, string? TypeDiscriminatorId)> GetSupportedDerivedTypes() + { + foreach (JsonDerivedTypeAttribute attribute in _derivedTypeAttributes) + { + yield return (attribute.DerivedType, attribute.TypeDiscriminatorId); + } + } +#pragma warning restore CA2252 // This API requires opting into preview features + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/IJsonPolymorphicTypeConfiguration.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/IJsonPolymorphicTypeConfiguration.cs new file mode 100644 index 00000000000000..0b01c280f309c0 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/IJsonPolymorphicTypeConfiguration.cs @@ -0,0 +1,18 @@ +// 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; + +namespace System.Text.Json.Serialization.Metadata +{ + internal interface IJsonPolymorphicTypeConfiguration + { +#pragma warning disable CA2252 // This API requires opting into preview features + Type BaseType { get; } + string? CustomTypeDiscriminatorPropertyName { get; } + JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; } + bool IgnoreUnrecognizedTypeDiscriminators { get; } + IEnumerable<(Type DerivedType, string? TypeDiscriminatorId)> GetSupportedDerivedTypes(); +#pragma warning restore CA2252 // This API requires opting into preview features + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index b1996066b3434c..af841f1e0c4f69 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -34,6 +34,8 @@ public partial class JsonTypeInfo internal JsonPropertyInfo? DataExtensionProperty { get; set; } + internal PolymorphicTypeResolver? PolymorphicTypeResolver { get; private set; } + // If enumerable or dictionary, the JsonTypeInfo for the element type. private JsonTypeInfo? _elementTypeInfo; @@ -168,6 +170,7 @@ internal JsonTypeInfo(Type type, JsonConverter converter, JsonSerializerOptions Options = options; PropertyInfoForTypeInfo = CreatePropertyInfoForTypeInfo(Type, converter, Options, this); ElementType = converter.ElementType; + ConfigurePolymorphism(converter, options); switch (PropertyInfoForTypeInfo.ConverterStrategy) { @@ -453,6 +456,33 @@ private static bool IsByRefLike(Type type) #endif } + internal void ConfigurePolymorphism(JsonConverter converter, JsonSerializerOptions options) + { +#pragma warning disable CA2252 // This API requires opting into preview features + Debug.Assert(Type != null); + + IJsonPolymorphicTypeConfiguration? configuration = null; + + // 1. Look up configuration from JsonSerializerOptions + foreach (JsonPolymorphicTypeConfiguration config in options.PolymorphicTypeConfigurations) + { + if (config.BaseType == Type) + { + configuration = config; + } + } + + // 2. Look up configuration from attributes + configuration ??= AttributePolymorphicTypeConfiguration.Create(Type); + + // Construct the resolver from configuration. + if (configuration != null) + { + PolymorphicTypeResolver = new PolymorphicTypeResolver(converter, configuration, options); + } +#pragma warning restore CA2252 // This API requires opting into preview features + } + internal bool IsValidDataExtensionProperty(JsonPropertyInfo jsonPropertyInfo) { Type memberType = jsonPropertyInfo.PropertyType; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PolymorphicTypeResolver.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PolymorphicTypeResolver.cs new file mode 100644 index 00000000000000..460ec9f2965416 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PolymorphicTypeResolver.cs @@ -0,0 +1,252 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Validates and indexes polymorphic type configuration, + /// providing derived JsonTypeInfo resolution methods + /// in both serialization and deserialization scenaria. + /// + internal sealed class PolymorphicTypeResolver + { +#pragma warning disable CA2252 // This API requires opting into preview features + private readonly JsonSerializerOptions _options; + private readonly ConcurrentDictionary _typeToDiscriminatorId = new(); + private readonly Dictionary? _discriminatorIdtoType; + + public PolymorphicTypeResolver(JsonConverter baseConverter, IJsonPolymorphicTypeConfiguration configuration, JsonSerializerOptions options) + { + _options = options; + BaseType = configuration.BaseType; + UnknownDerivedTypeHandling = configuration.UnknownDerivedTypeHandling; + IgnoreUnrecognizedTypeDiscriminators = configuration.IgnoreUnrecognizedTypeDiscriminators; + + if (!IsSupportedPolymorphicBaseType(BaseType)) + { + ThrowHelper.ThrowInvalidOperationException_TypeDoesNotSupportPolymorphism(BaseType); + } + + bool containsDerivedTypes = false; + foreach ((Type derivedType, string? typeDiscriminatorId) in configuration.GetSupportedDerivedTypes()) + { + if (!IsSupportedDerivedType(BaseType, derivedType) || + (derivedType.IsAbstract && UnknownDerivedTypeHandling != JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor)) + { + ThrowHelper.ThrowInvalidOperationException_DerivedTypeNotSupported(BaseType, derivedType); + } + + if (typeDiscriminatorId is not null) + { + UsesTypeDiscriminators = true; + } + + var derivedJsonTypeInfo = new DerivedJsonTypeInfo(derivedType, typeDiscriminatorId); + + if (!_typeToDiscriminatorId.TryAdd(derivedType, derivedJsonTypeInfo)) + { + ThrowHelper.ThrowInvalidOperationException_DerivedTypeIsAlreadySpecified(BaseType, derivedType); + } + + if (typeDiscriminatorId is not null) + { + if (!(_discriminatorIdtoType ??= new()).TryAdd(typeDiscriminatorId, derivedJsonTypeInfo)) + { + ThrowHelper.ThrowInvalidOperationException_TypeDicriminatorIdIsAlreadySpecified(BaseType, typeDiscriminatorId); + } + } + + containsDerivedTypes = true; + } + + if (!containsDerivedTypes) + { + ThrowHelper.ThrowInvalidOperationException_PolymorphicTypeConfigurationDoesNotSpecifyDerivedTypes(BaseType); + } + + if (UsesTypeDiscriminators) + { + if (!baseConverter.CanHaveMetadata) + { + ThrowHelper.ThrowNotSupportedException_BaseConverterDoesNotSupportMetadata(BaseType); + } + + if (configuration.CustomTypeDiscriminatorPropertyName is string customPropertyName) + { + JsonEncodedText jsonEncodedName = JsonEncodedText.Encode(customPropertyName, options.Encoder); + + // Check if the property name conflicts with other metadata property names + if ((JsonSerializer.GetMetadataPropertyName(jsonEncodedName.EncodedUtf8Bytes, resolver: null) & ~MetadataPropertyName.Type) != 0) + { + ThrowHelper.ThrowInvalidOperationException_InvalidCustomTypeDiscriminatorPropertyName(); + } + + CustomTypeDiscriminatorPropertyName = customPropertyName; + CustomTypeDiscriminatorPropertyNameUtf8 = jsonEncodedName.EncodedUtf8Bytes.ToArray(); + CustomTypeDiscriminatorPropertyNameJsonEncoded = jsonEncodedName; + } + } + } + + public Type BaseType { get; } + public JsonUnknownDerivedTypeHandling UnknownDerivedTypeHandling { get; } + public bool UsesTypeDiscriminators { get; } + public bool IgnoreUnrecognizedTypeDiscriminators { get; } + public string? CustomTypeDiscriminatorPropertyName { get; } + public byte[]? CustomTypeDiscriminatorPropertyNameUtf8 { get; } + public JsonEncodedText? CustomTypeDiscriminatorPropertyNameJsonEncoded { get; } + + public bool TryGetDerivedJsonTypeInfo(Type runtimeType, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo, out string? typeDiscriminatorId) + { + Debug.Assert(BaseType.IsAssignableFrom(runtimeType)); + + if (!_typeToDiscriminatorId.TryGetValue(runtimeType, out DerivedJsonTypeInfo? result)) + { + switch (UnknownDerivedTypeHandling) + { + case JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor: + // Calculate (and cache the result) of the nearest ancestor for given runtime type. + // A `null` result denotes no matching ancestor type, we also cache that. + result = CalculateNearestAncestor(runtimeType); + _typeToDiscriminatorId[runtimeType] = result; + break; + case JsonUnknownDerivedTypeHandling.FallbackToBaseType: + // Recover the polymorphic contract (i.e. any type discriminators) for the base type, if it exists. + _typeToDiscriminatorId.TryGetValue(BaseType, out result); + _typeToDiscriminatorId[runtimeType] = result; + break; + + case JsonUnknownDerivedTypeHandling.FailSerialization: + default: + if (runtimeType != BaseType) + { + ThrowHelper.ThrowNotSupportedException_RuntimeTypeNotSupported(BaseType, runtimeType); + } + break; + } + } + + if (result is null) + { + jsonTypeInfo = null; + typeDiscriminatorId = null; + return false; + } + else + { + jsonTypeInfo = result.GetJsonTypeInfo(_options); + typeDiscriminatorId = result.TypeDiscriminatorId; + return true; + } + } + + public bool TryGetDerivedJsonTypeInfo(string typeDiscriminatorId, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo) + { + Debug.Assert(UsesTypeDiscriminators); + Debug.Assert(_discriminatorIdtoType != null); + + if (_discriminatorIdtoType.TryGetValue(typeDiscriminatorId, out DerivedJsonTypeInfo? result)) + { + Debug.Assert(result.TypeDiscriminatorId == typeDiscriminatorId); + jsonTypeInfo = result.GetJsonTypeInfo(_options); + return true; + } + + if (!IgnoreUnrecognizedTypeDiscriminators) + { + ThrowHelper.ThrowJsonException_UnrecognizedTypeDiscriminator(typeDiscriminatorId); + } + + jsonTypeInfo = null; + return false; + } + + public static bool IsSupportedPolymorphicBaseType(Type? type) => + type != null && + (type.IsClass || type.IsInterface) && + !type.IsSealed && + !type.IsGenericTypeDefinition && + !type.IsPointer && + type != JsonTypeInfo.ObjectType; + + public static bool IsSupportedDerivedType(Type baseType, Type? derivedType) => + baseType.IsAssignableFrom(derivedType) && !derivedType.IsGenericTypeDefinition; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070:UnrecognizedReflectionPattern", + Justification = "The call to GetInterfaces will cross-reference results with interface types " + + "already declared as derived types of the polymorphic base type.")] + private DerivedJsonTypeInfo? CalculateNearestAncestor(Type type) + { + Debug.Assert(!type.IsAbstract); + Debug.Assert(BaseType.IsAssignableFrom(type)); + Debug.Assert(UnknownDerivedTypeHandling == JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor); + + if (type == BaseType) + { + return null; + } + + DerivedJsonTypeInfo? result = null; + + // First, walk up the class hierarchy for any suported types. + for (Type? candidate = type.BaseType; BaseType.IsAssignableFrom(candidate); candidate = candidate.BaseType) + { + Debug.Assert(candidate != null); + + if (_typeToDiscriminatorId.TryGetValue(candidate, out result)) + { + break; + } + } + + // Interface hierarchies admit the possibility of diamond ambiguities in type discriminators. + // Examine all interface implementations and identify potential conflicts. + if (BaseType.IsInterface) + { + foreach (Type interfaceTy in type.GetInterfaces()) + { + if (interfaceTy != BaseType && BaseType.IsAssignableFrom(interfaceTy) && + _typeToDiscriminatorId.TryGetValue(interfaceTy, out DerivedJsonTypeInfo? interfaceResult) && + interfaceResult is not null) + { + if (result is null) + { + result = interfaceResult; + } + else + { + ThrowHelper.ThrowNotSupportedException_RuntimeTypeDiamondAmbiguity(BaseType, type, result.DerivedType, interfaceResult.DerivedType); + } + } + } + } + + return result; + } + + /// + /// Lazy JsonTypeInfo result holder for a derived type. + /// + private class DerivedJsonTypeInfo + { + private volatile JsonTypeInfo? _jsonTypeInfo; + + public DerivedJsonTypeInfo(Type type, string? typeDiscriminatorId) + { + DerivedType = type; + TypeDiscriminatorId = typeDiscriminatorId; + } + + public Type DerivedType { get; } + public string? TypeDiscriminatorId { get; } + public JsonTypeInfo GetJsonTypeInfo(JsonSerializerOptions options) + => _jsonTypeInfo ??= options.GetOrAddJsonTypeInfo(DerivedType); + } +#pragma warning restore CA2252 // This API requires opting into preview features + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs index 112476b1a9191c..2b022c507f5903 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/MetadataPropertyName.cs @@ -6,9 +6,10 @@ namespace System.Text.Json [Flags] internal enum MetadataPropertyName : byte { - None = 0, - Values = 1, - Id = 2, - Ref = 4, + None = 0, + Values = 1, + Id = 2, + Ref = 4, + Type = 8, } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PolymorphicSerializationState.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PolymorphicSerializationState.cs index 735c10e22af990..442a9975bb7c2e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PolymorphicSerializationState.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PolymorphicSerializationState.cs @@ -8,13 +8,19 @@ internal enum PolymorphicSerializationState : byte None, /// - /// Dispatch to a polymorphic converter has been initiated. + /// Dispatch to a derived converter has been initiated. /// PolymorphicReEntryStarted, /// - /// Current frame is a continuation using a suspended polymorphic converter. + /// Current frame is a continuation using a suspended derived converter. /// - PolymorphicReEntrySuspended + PolymorphicReEntrySuspended, + + /// + /// Current frame is a polymorphic converter that couldn't resolve a derived converter. + /// (E.g. because the runtime type matches the declared type). + /// + PolymorphicReEntryNotFound } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs index ebc2799cdefd05..8fe1b46b115728 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStack.cs @@ -67,14 +67,14 @@ internal struct ReadStack public string? ReferenceId; /// - /// Whether we can read without the need of saving state for stream and preserve references cases. + /// Holds the value of $type of the currently read object /// - public bool UseFastPath; + public string? PolymorphicTypeDiscriminator; /// - /// Global flag indicating whether the current deserializer supports metadata. + /// Global flag indicating whether we can read preserved references. /// - public bool CanContainMetadata; + public bool PreserveReferences; /// /// Ensures that the stack buffer has sufficient capacity to hold an additional frame. @@ -103,14 +103,14 @@ internal void Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinuation = f if (options.ReferenceHandlingStrategy == ReferenceHandlingStrategy.Preserve) { ReferenceResolver = options.ReferenceHandler!.CreateResolver(writing: false); - CanContainMetadata = true; + PreserveReferences = true; } SupportContinuation = supportContinuation; Current.JsonTypeInfo = jsonTypeInfo; Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; Current.NumberHandling = Current.JsonPropertyInfo.EffectiveNumberHandling; - UseFastPath = !supportContinuation && !CanContainMetadata; + Current.CanContainMetadata = PreserveReferences || jsonTypeInfo.PolymorphicTypeResolver?.UsesTypeDiscriminators == true; } public void Push() @@ -128,7 +128,6 @@ public void Push() { JsonTypeInfo jsonTypeInfo = Current.JsonPropertyInfo?.JsonTypeInfo ?? Current.CtorArgumentState!.JsonParameterInfo!.JsonTypeInfo; JsonNumberHandling? numberHandling = Current.NumberHandling; - ConverterStrategy converterStrategy = Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterStrategy; EnsurePushCapacity(); _stack[_count - 1] = Current; @@ -139,11 +138,13 @@ public void Push() Current.JsonPropertyInfo = jsonTypeInfo.PropertyInfoForTypeInfo; // Allow number handling on property to win over handling on type. Current.NumberHandling = numberHandling ?? Current.JsonPropertyInfo.EffectiveNumberHandling; + Current.CanContainMetadata = PreserveReferences || jsonTypeInfo.PolymorphicTypeResolver?.UsesTypeDiscriminators == true; } } else { - // We are re-entering a continuation, adjust indices accordingly + // We are re-entering a continuation, adjust indices accordingly. + if (_count++ > 0) { _stack[_count - 2] = Current; @@ -167,6 +168,7 @@ public void Push() public void Pop(bool success) { Debug.Assert(_count > 0); + Debug.Assert(JsonPath() is not null); if (!success) { @@ -208,6 +210,53 @@ public void Pop(bool success) SetConstructorArgumentState(); } + /// + /// Configures the current stack frame for a polymorphic converter. + /// + public JsonConverter InitializePolymorphicReEntry(JsonTypeInfo derivedJsonTypeInfo) + { + Debug.Assert(!IsContinuation); + Debug.Assert(Current.PolymorphicJsonTypeInfo == null); + Debug.Assert(Current.PolymorphicSerializationState == PolymorphicSerializationState.None); + + Current.PolymorphicJsonTypeInfo = Current.JsonTypeInfo; + Current.JsonTypeInfo = derivedJsonTypeInfo.PropertyInfoForTypeInfo.JsonTypeInfo; + Current.JsonPropertyInfo = Current.JsonTypeInfo.PropertyInfoForTypeInfo; + Current.NumberHandling ??= Current.JsonPropertyInfo.NumberHandling; + Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + SetConstructorArgumentState(); + + return derivedJsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + } + + + /// + /// Configures the current frame for a continuation of a polymorphic converter. + /// + public JsonConverter ResumePolymorphicReEntry() + { + Debug.Assert(Current.PolymorphicJsonTypeInfo != null); + Debug.Assert(Current.PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntrySuspended); + + // Swap out the two values as we resume the polymorphic converter + (Current.JsonTypeInfo, Current.PolymorphicJsonTypeInfo) = (Current.PolymorphicJsonTypeInfo, Current.JsonTypeInfo); + Current.PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + return Current.JsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; + } + + /// + /// Updates frame state after a polymorphic converter has returned. + /// + public void ExitPolymorphicConverter(bool success) + { + Debug.Assert(Current.PolymorphicJsonTypeInfo != null); + Debug.Assert(Current.PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntryStarted); + + // Swap out the two values as we exit the polymorphic converter + (Current.JsonTypeInfo, Current.PolymorphicJsonTypeInfo) = (Current.PolymorphicJsonTypeInfo, Current.JsonTypeInfo); + Current.PolymorphicSerializationState = success ? PolymorphicSerializationState.None : PolymorphicSerializationState.PolymorphicReEntrySuspended; + } + // Return a JSONPath using simple dot-notation when possible. When special characters are present, bracket-notation is used: // $.x.y[0].z // $['PropertyName.With.Special.Chars'] @@ -217,7 +266,7 @@ public string JsonPath() (int frameCount, bool includeCurrentFrame) = _continuationCount switch { - 0 => (_count - 1, true), // Not a countinuation, report previous frames and Current. + 0 => (_count - 1, true), // Not a continuation, report previous frames and Current. 1 => (0, true), // Continuation of depth 1, just report Current frame. int c => (c, false) // Continuation of depth > 1, report the entire stack. }; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index 948cff4b3087ed..f1440a240bab46 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -36,9 +36,23 @@ internal struct ReadStackFrame public JsonTypeInfo JsonTypeInfo; public StackFrameObjectState ObjectState; // State tracking the current object. + // Current object can contain metadata + public bool CanContainMetadata; public MetadataPropertyName LatestMetadataPropertyName; public MetadataPropertyName MetadataPropertyNames; + // Serialization state for value serialized by the current frame. + public PolymorphicSerializationState PolymorphicSerializationState; + + // Holds any entered polymorphic JsonTypeInfo metadata. + public JsonTypeInfo? PolymorphicJsonTypeInfo; + + // Gets the initial JsonTypeInfo metadata used when deserializing the current value. + public JsonTypeInfo BaseJsonTypeInfo + => PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntryStarted + ? PolymorphicJsonTypeInfo! + : JsonTypeInfo; + // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker. public int PropertyIndex; public List? PropertyRefCache; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs index 83ba7d51aa5fac..b383da064b212b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs @@ -22,6 +22,19 @@ internal struct WriteStack /// public WriteStackFrame Current; + /// + /// Gets the parent stackframe, if it exists. + /// + public ref WriteStackFrame Parent + { + get + { + Debug.Assert(_count - _indexOffset > 0); + Debug.Assert(_stack is not null); + return ref _stack[_count - _indexOffset - 1]; + } + } + /// /// Buffer containing all frames in the stack. For performance it is only populated for serialization depths > 1. /// @@ -37,6 +50,14 @@ internal struct WriteStack /// private int _continuationCount; + /// + /// Offset used to derive the index of the current frame in the stack buffer from the current value of , + /// following the formula currentIndex := _count - _indexOffset. + /// Value can vary between 0 or 1 depending on whether we need to allocate a new frame on the first Push() operation, + /// which can happen if the root converter is polymorphic. + /// + private byte _indexOffset; + /// /// Cancellation token used by converters performing async serialization (e.g. IAsyncEnumerable) /// @@ -88,6 +109,16 @@ internal struct WriteStack /// public string? NewReferenceId; + /// + /// Indicates that the next converter is polymorphic and must serialize a type discriminator. + /// + public string? PolymorphicTypeDiscriminator; + + /// + /// Whether the current frame needs to write out any metadata. + /// + public bool CurrentContainsMetadata => NewReferenceId != null || PolymorphicTypeDiscriminator != null; + private void EnsurePushCapacity() { if (_stack is null) @@ -130,16 +161,27 @@ internal JsonConverter Initialize(JsonTypeInfo jsonTypeInfo, bool supportContinu return jsonTypeInfo.PropertyInfoForTypeInfo.ConverterBase; } + /// + /// Gets the nested JsonTypeInfo before resolving any polymorphic converters + /// + public JsonTypeInfo PeekNestedJsonTypeInfo() + { + Debug.Assert(Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted); + return _count == 0 ? Current.JsonTypeInfo : Current.JsonPropertyInfo!.JsonTypeInfo; + } + public void Push() { if (_continuationCount == 0) { - if (_count == 0) + Debug.Assert(Current.PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntrySuspended); + + if (_count == 0 && Current.PolymorphicSerializationState == PolymorphicSerializationState.None) { - // Performance optimization: reuse the first stackframe on the first push operation. - // NB need to be careful when making writes to Current _before_ the first `Push` - // operation is performed. + // Perf enhancement: do not create a new stackframe on the first push operation + // unless the converter has primed the current frame for polymorphic dispatch. _count = 1; + _indexOffset = 1; // currentIndex := _count - 1; } else { @@ -147,7 +189,7 @@ public void Push() JsonNumberHandling? numberHandling = Current.NumberHandling; EnsurePushCapacity(); - _stack[_count - 1] = Current; + _stack[_count - _indexOffset] = Current; Current = default; _count++; @@ -160,9 +202,9 @@ public void Push() else { // We are re-entering a continuation, adjust indices accordingly - if (_count++ > 0) + if (_count++ > 0 || _indexOffset == 0) { - Current = _stack[_count - 1]; + Current = _stack[_count - _indexOffset]; } // check if we are done @@ -187,7 +229,7 @@ public void Pop(bool success) // Check if we need to initialize the continuation. if (_continuationCount == 0) { - if (_count == 1) + if (_count == 1 && _indexOffset > 0) { // No need to copy any frames here. _continuationCount = 1; @@ -200,22 +242,23 @@ public void Pop(bool success) EnsurePushCapacity(); _continuationCount = _count--; } - else if (--_count == 0) + else if (--_count == 0 && _indexOffset > 0) { // reached the root, no need to copy frames. return; } - _stack[_count] = Current; - Current = _stack[_count - 1]; + int currentIndex = _count - _indexOffset; + _stack[currentIndex + 1] = Current; + Current = _stack[currentIndex]; } else { Debug.Assert(_continuationCount == 0); - if (--_count > 0) + if (--_count > 0 || _indexOffset == 0) { - Current = _stack[_count - 1]; + Current = _stack[_count - _indexOffset]; } } } @@ -342,14 +385,14 @@ public string PropertyPath() (int frameCount, bool includeCurrentFrame) = _continuationCount switch { - 0 => (_count - 1, true), // Not a countinuation, report previous frames and Current. + 0 => (_count - 1, true), // Not a continuation, report previous frames and Current. 1 => (0, true), // Continuation of depth 1, just report Current frame. int c => (c, false) // Continuation of depth > 1, report the entire stack. }; - for (int i = 0; i < frameCount; i++) + for (int i = 1; i <= frameCount; i++) { - AppendStackFrame(sb, ref _stack[i]); + AppendStackFrame(sb, ref _stack[i - _indexOffset]); } if (includeCurrentFrame) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs index 1bb059dc7112b9..0e9fc8c63fbc27 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs @@ -81,9 +81,15 @@ internal struct WriteStackFrame public bool IsPushedReferenceForCycleDetection; - public void EndDictionaryElement() + public void EndCollectionElement() + { + PolymorphicSerializationState = PolymorphicSerializationState.None; + } + + public void EndDictionaryEntry() { PropertyState = StackFramePropertyState.None; + PolymorphicSerializationState = PolymorphicSerializationState.None; } public void EndProperty() @@ -91,6 +97,7 @@ public void EndProperty() JsonPropertyInfo = null!; JsonPropertyNameAsString = null; PropertyState = StackFramePropertyState.None; + PolymorphicSerializationState = PolymorphicSerializationState.None; } /// @@ -107,25 +114,11 @@ public JsonTypeInfo GetNestedJsonTypeInfo() } /// - /// Initializes the state for polymorphic cases and returns the appropriate converter. + /// Configures the next stack frame for a polymorphic converter. /// - public JsonConverter? ResolvePolymorphicConverter(object value, Type typeToConvert, JsonSerializerOptions options) + public JsonConverter InitializePolymorphicReEntry(Type runtimeType, JsonSerializerOptions options) { - Debug.Assert(value != null); - Debug.Assert(PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted); - - if (PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntrySuspended) - { - // Quickly retrieve the polymorphic converter in case of a re-entrant continuation - Debug.Assert(PolymorphicJsonTypeInfo != null && value.GetType() == PolymorphicJsonTypeInfo.PropertyType); - return PolymorphicJsonTypeInfo.ConverterBase; - } - - Type runtimeType = value.GetType(); - if (runtimeType == typeToConvert) - { - return null; - } + Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.None); // For perf, avoid the dictionary lookup in GetOrAddJsonTypeInfo() for every element of a collection // if the current element is the same type as the previous element. @@ -135,18 +128,38 @@ public JsonTypeInfo GetNestedJsonTypeInfo() PolymorphicJsonTypeInfo = typeInfo.PropertyInfoForTypeInfo; } + PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; return PolymorphicJsonTypeInfo.ConverterBase; } - public void EnterPolymorphicConverter() + /// + /// Configures the next stack frame for a polymorphic converter. + /// + public JsonConverter InitializePolymorphicReEntry(JsonTypeInfo derivedJsonTypeInfo) { - Debug.Assert(PolymorphicSerializationState != PolymorphicSerializationState.PolymorphicReEntryStarted); + Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.None); + + PolymorphicJsonTypeInfo = derivedJsonTypeInfo.PropertyInfoForTypeInfo; PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + return PolymorphicJsonTypeInfo.ConverterBase; } + /// + /// Configures the next frame for a continuation of a polymorphic converter. + /// + public JsonConverter ResumePolymorphicReEntry() + { + Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntrySuspended); + Debug.Assert(PolymorphicJsonTypeInfo is not null); + PolymorphicSerializationState = PolymorphicSerializationState.PolymorphicReEntryStarted; + return PolymorphicJsonTypeInfo.ConverterBase; + } + + /// + /// Updates frame state after a polymorphic converter has returned. + /// public void ExitPolymorphicConverter(bool success) { - Debug.Assert(PolymorphicSerializationState == PolymorphicSerializationState.PolymorphicReEntryStarted); PolymorphicSerializationState = success ? PolymorphicSerializationState.None : PolymorphicSerializationState.PolymorphicReEntrySuspended; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 51af633d11381f..6b486ffa9d0384 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -471,6 +471,13 @@ public static void ThrowJsonException_MetadataReferenceObjectCannotContainOtherP ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(); } + [DoesNotReturn] + public static void ThrowJsonException_MetadataUnexpectedProperty(ReadOnlySpan propertyName, ref ReadStack state) + { + state.Current.JsonPropertyName = propertyName.ToArray(); + ThrowJsonException(SR.Format(SR.MetadataUnexpectedProperty)); + } + [DoesNotReturn] public static void ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties() { @@ -485,10 +492,10 @@ public static void ThrowJsonException_MetadataIdIsNotFirstProperty(ReadOnlySpan< } [DoesNotReturn] - public static void ThrowJsonException_MetadataMissingIdBeforeValues(ref ReadStack state, ReadOnlySpan propertyName) + public static void ThrowJsonException_MetadataStandaloneValuesProperty(ref ReadStack state, ReadOnlySpan propertyName) { state.Current.JsonPropertyName = propertyName.ToArray(); - ThrowJsonException(SR.MetadataPreservedArrayPropertyNotFound); + ThrowJsonException(SR.MetadataStandaloneValuesProperty); } [DoesNotReturn] @@ -513,6 +520,12 @@ public static void ThrowJsonException_MetadataDuplicateIdFound(string id) ThrowJsonException(SR.Format(SR.MetadataDuplicateIdFound, id)); } + [DoesNotReturn] + public static void ThrowJsonException_MetadataDuplicateTypeProperty() + { + ThrowJsonException(SR.MetadataDuplicateTypeProperty); + } + [DoesNotReturn] public static void ThrowJsonException_MetadataInvalidReferenceToValueType(Type propertyType) { @@ -520,13 +533,13 @@ public static void ThrowJsonException_MetadataInvalidReferenceToValueType(Type p } [DoesNotReturn] - public static void ThrowJsonException_MetadataPreservedArrayInvalidProperty(ref ReadStack state, Type propertyType, in Utf8JsonReader reader) + public static void ThrowJsonException_MetadataInvalidPropertyInArrayMetadata(ref ReadStack state, Type propertyType, in Utf8JsonReader reader) { state.Current.JsonPropertyName = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray(); string propertyNameAsString = reader.GetString()!; ThrowJsonException(SR.Format(SR.MetadataPreservedArrayFailed, - SR.Format(SR.MetadataPreservedArrayInvalidProperty, propertyNameAsString), + SR.Format(SR.MetadataInvalidPropertyInArrayMetadata, propertyNameAsString), SR.Format(SR.DeserializeUnableToConvertValue, propertyType))); } @@ -537,7 +550,7 @@ public static void ThrowJsonException_MetadataPreservedArrayValuesNotFound(ref R state.Current.JsonPropertyName = null; ThrowJsonException(SR.Format(SR.MetadataPreservedArrayFailed, - SR.MetadataPreservedArrayPropertyNotFound, + SR.MetadataStandaloneValuesProperty, SR.Format(SR.DeserializeUnableToConvertValue, propertyType))); } @@ -560,14 +573,10 @@ internal static void ThrowUnexpectedMetadataException( ref ReadStack state) { - MetadataPropertyName name = JsonSerializer.GetMetadataPropertyName(propertyName); - if (name == MetadataPropertyName.Id) - { - ThrowJsonException_MetadataIdIsNotFirstProperty(propertyName, ref state); - } - else if (name == MetadataPropertyName.Ref) + MetadataPropertyName name = JsonSerializer.GetMetadataPropertyName(propertyName, state.Current.BaseJsonTypeInfo.PolymorphicTypeResolver); + if (name != 0) { - ThrowJsonException_MetadataReferenceObjectCannotContainOtherProperties(propertyName, ref state); + ThrowJsonException_MetadataUnexpectedProperty(propertyName, ref state); } else { @@ -619,5 +628,71 @@ public static void ThrowMissingMemberException_MissingFSharpCoreMember(string mi { throw new MissingMemberException(SR.Format(SR.MissingFSharpCoreMember, missingFsharpCoreMember)); } + + [DoesNotReturn] + public static void ThrowNotSupportedException_BaseConverterDoesNotSupportMetadata(Type derivedType) + { + throw new NotSupportedException(SR.Format(SR.Polymorphism_DerivedConverterDoesNotSupportMetadata, derivedType)); + } + + [DoesNotReturn] + public static void ThrowNotSupportedException_DerivedConverterDoesNotSupportMetadata(Type derivedType) + { + throw new NotSupportedException(SR.Format(SR.Polymorphism_DerivedConverterDoesNotSupportMetadata, derivedType)); + } + + [DoesNotReturn] + public static void ThrowNotSupportedException_RuntimeTypeNotSupported(Type baseType, Type runtimeType) + { + throw new NotSupportedException(SR.Format(SR.Polymorphism_RuntimeTypeNotSupported, runtimeType, baseType)); + } + + [DoesNotReturn] + public static void ThrowNotSupportedException_RuntimeTypeDiamondAmbiguity(Type baseType, Type runtimeType, Type derivedType1, Type derivedType2) + { + throw new NotSupportedException(SR.Format(SR.Polymorphism_RuntimeTypeDiamondAmbiguity, runtimeType, derivedType1, derivedType2, baseType)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_TypeDoesNotSupportPolymorphism(Type baseType) + { + throw new InvalidOperationException(SR.Format(SR.Polymorphism_TypeDoesNotSupportPolymorphism, baseType)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_DerivedTypeNotSupported(Type baseType, Type derivedType) + { + throw new InvalidOperationException(SR.Format(SR.Polymorphism_DerivedTypeIsNotSupported, derivedType, baseType)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_DerivedTypeIsAlreadySpecified(Type baseType, Type derivedType) + { + throw new InvalidOperationException(SR.Format(SR.Polymorphism_DerivedTypeIsAlreadySpecified, baseType, derivedType)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_TypeDicriminatorIdIsAlreadySpecified(Type baseType, string typeDiscriminatorId) + { + throw new InvalidOperationException(SR.Format(SR.Polymorphism_TypeDicriminatorIdIsAlreadySpecified, baseType, typeDiscriminatorId)); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_InvalidCustomTypeDiscriminatorPropertyName() + { + throw new InvalidOperationException(SR.Polymorphism_InvalidCustomTypeDiscriminatorPropertyName); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_PolymorphicTypeConfigurationDoesNotSpecifyDerivedTypes(Type baseType) + { + throw new InvalidOperationException(SR.Format(SR.Polymorphism_ConfigurationDoesNotSpecifyDerivedTypes, baseType)); + } + + [DoesNotReturn] + public static void ThrowJsonException_UnrecognizedTypeDiscriminator(string typeDiscriminatorId) + { + ThrowJsonException(SR.Format(SR.Polymorphism_UnrecognizedTypeDiscriminator, typeDiscriminatorId)); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs index 39af73f2aff9cb..83790f71b2cf79 100644 --- a/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs +++ b/src/libraries/System.Text.Json/tests/Common/ReferenceHandlerTests/ReferenceHandlerTests.Deserialize.cs @@ -914,7 +914,21 @@ public async Task ThrowOnStructWithReference() [InlineData(@"{""$iz"": ""1""}", "$.$iz")] [InlineData(@"{""$rez"": ""1""}", "$.$rez")] [InlineData(@"{""$valuez"": []}", "$.$valuez")] - public async Task InvalidMetadataPropertyNameWithSameLengthIsNotRecognized(string json, string expectedPath) + [InlineData(@"{""$type"": ""derivedType""}", "$.$type")] + [InlineData(@"{""$PropertyWithDollarSign"": ""1""}", "$.$PropertyWithDollarSign")] + [InlineData(@"{""$id"" : ""1"", ""$iz"": ""1""}", "$.$iz")] + [InlineData(@"{""$id"" : ""1"", ""$rez"": ""1""}", "$.$rez")] + [InlineData(@"{""$id"" : ""1"", ""$id"" : 1 }", "$.$id")] + [InlineData(@"{""$id"" : ""1"", ""$ref"" : 1 }", "$.$ref")] + [InlineData(@"{""$id"" : ""1"", ""$valuez"": ""[]""}", "$.$valuez")] + [InlineData(@"{""$id"" : ""1"", ""$type"": ""derivedType""}", "$.$type")] + [InlineData(@"{""$id"" : ""1"", ""$PropertyWithDollarSign"": ""1""}", "$.$PropertyWithDollarSign")] + [InlineData(@"{""$id"" : ""1"", ""NonMetadataProperty"" : 42, ""$iz"": ""1""}", "$.$iz")] + [InlineData(@"{""$id"" : ""1"", ""NonMetadataProperty"" : 42, ""$rez"": ""1""}", "$.$rez")] + [InlineData(@"{""$id"" : ""1"", ""NonMetadataProperty"" : 42, ""$valuez"": ""[]""}", "$.$valuez")] + [InlineData(@"{""$id"" : ""1"", ""NonMetadataProperty"" : 42, ""$type"": ""derivedType""}", "$.$type")] + [InlineData(@"{""$id"" : ""1"", ""NonMetadataProperty"" : 42, ""$PropertyWithDollarSign"": ""1""}", "$.$PropertyWithDollarSign")] + public async Task InvalidMetadataPropertyNameIsRejected(string json, string expectedPath) { JsonException ex = await Assert.ThrowsAsync(async () => await Serializer.DeserializeWrapper(json, s_deserializerOptionsPreserve)); Assert.Equal(expectedPath, ex.Path); diff --git a/src/libraries/System.Text.Json/tests/Common/SerializerTests.cs b/src/libraries/System.Text.Json/tests/Common/SerializerTests.cs index fe37ff6ce0da11..da32de3f29e393 100644 --- a/src/libraries/System.Text.Json/tests/Common/SerializerTests.cs +++ b/src/libraries/System.Text.Json/tests/Common/SerializerTests.cs @@ -1,6 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Xunit; + namespace System.Text.Json.Serialization.Tests { /// @@ -8,6 +15,12 @@ namespace System.Text.Json.Serialization.Tests /// public abstract class SerializerTests { + protected SerializerTests(JsonSerializerWrapper serializerUnderTest) + { + Serializer = serializerUnderTest; + StreamingSerializer = serializerUnderTest as StreamingJsonSerializerWrapper; + } + /// /// The serialization System Under Test to be targeted by deriving test suites. /// @@ -18,10 +31,291 @@ public abstract class SerializerTests /// protected StreamingJsonSerializerWrapper? StreamingSerializer { get; } - protected SerializerTests(JsonSerializerWrapper serializerUnderTest) + [Flags] + protected enum SerializedValueContext { - Serializer = serializerUnderTest; - StreamingSerializer = serializerUnderTest as StreamingJsonSerializerWrapper; + None = 0, + RootValue = 1, + ObjectProperty = 2, + CollectionElement = 4, + DictionaryValue = 8, + JsonNode = 16, + All = RootValue | ObjectProperty | CollectionElement | DictionaryValue | JsonNode + } + + /// + /// Tests serialization of a given value within the context of multiple types: + /// root values, object properties, collection elements, dictionary values, etc. + /// + protected async Task TestMultiContextSerialization( + TValue value, + string expectedJson, + Type? expectedExceptionType = null, + SerializedValueContext contexts = SerializedValueContext.All, + JsonSerializerOptions? options = null) + { + Assert.True((contexts & SerializedValueContext.All) != SerializedValueContext.None); + + string actualJson; + + if (contexts.HasFlag(SerializedValueContext.RootValue)) + { + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.SerializeWrapper(value, options)); + } + else + { + actualJson = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + } + + if (contexts.HasFlag(SerializedValueContext.ObjectProperty)) + { + var poco = new GenericPoco { Property = value }; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.SerializeWrapper(poco, options)); + } + else + { + string propertyName = options?.PropertyNamingPolicy is JsonNamingPolicy policy + ? policy.ConvertName(nameof(GenericPoco.Property)) + : nameof(GenericPoco.Property); + + actualJson = await Serializer.SerializeWrapper(poco, options); + JsonTestHelper.AssertJsonEqual($@"{{ ""{propertyName}"" : {expectedJson} }}", actualJson); + } + } + + if (contexts.HasFlag(SerializedValueContext.CollectionElement)) + { + var list = new List { value }; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.SerializeWrapper(list, options)); + } + else + { + actualJson = await Serializer.SerializeWrapper(list, options); + JsonTestHelper.AssertJsonEqual($"[{expectedJson}]", actualJson); + } + } + + if (contexts.HasFlag(SerializedValueContext.DictionaryValue)) + { + const string key = "key"; + var dictionary = new Dictionary { [key] = value }; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.SerializeWrapper(dictionary, options)); + } + else + { + string jsonKey = options?.DictionaryKeyPolicy is JsonNamingPolicy policy + ? policy.ConvertName(key) + : key; + + actualJson = await Serializer.SerializeWrapper(dictionary, options); + JsonTestHelper.AssertJsonEqual($@"{{ ""{jsonKey}"" : {expectedJson} }}", actualJson); + } + } + + if (contexts.HasFlag(SerializedValueContext.JsonNode)) + { + const string key = "key"; + var jsonObject = new JsonObject { [key] = JsonValue.Create(value) }; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.SerializeWrapper(jsonObject, options)); + } + else + { + actualJson = await Serializer.SerializeWrapper(jsonObject, options); + JsonTestHelper.AssertJsonEqual($@"{{ ""{key}"" : {expectedJson} }}", actualJson); + } + } + } + + /// + /// Tests serialization of a given list of values within the context of multiple types: + /// root values, object properties, collection elements, dictionary values, etc. + /// + protected async Task TestMultiContextSerialization( + IEnumerable<(TValue Value, string ExpectedJson)> inputs, + SerializedValueContext contexts = SerializedValueContext.All, + JsonSerializerOptions? options = null) + { + inputs = inputs.ToList(); + string expectedJson = $"[{string.Join(", ", inputs.Select(x => x.ExpectedJson))}]"; + List values = inputs.Select(x => x.Value).ToList(); + await TestMultiContextSerialization( + values, + expectedJson, + expectedExceptionType: null, + contexts, + options); + } + + /// + /// Tests deserialization of a given value within the context of multiple types: + /// root values, object properties, collection elements, dictionary values, etc. + /// + protected async Task TestMultiContextDeserialization( + string json, + TValue? expectedValue = default, + Type? expectedExceptionType = null, + SerializedValueContext contexts = SerializedValueContext.All, + JsonSerializerOptions? options = null, + IEqualityComparer? equalityComparer = null) + { + Assert.True((contexts & SerializedValueContext.All) != SerializedValueContext.None); + + string wrappedJson; + equalityComparer ??= expectedValue is IEquatable + ? EqualityComparer.Default + : new JsonEqualityComparer(); + + if (contexts.HasFlag(SerializedValueContext.RootValue)) + { + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.DeserializeWrapper(json, options)); + } + else + { + TValue value = await Serializer.DeserializeWrapper(json, options); + Assert.Equal(expectedValue, value, equalityComparer); + } + } + + if (contexts.HasFlag(SerializedValueContext.ObjectProperty)) + { + string propertyName = + options?.PropertyNamingPolicy is JsonNamingPolicy policy + ? policy.ConvertName(nameof(GenericPoco.Property)) + : nameof(GenericPoco.Property); + + wrappedJson = $@"{{ ""{propertyName}"" : {json} }}"; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.DeserializeWrapper>(wrappedJson, options)); + } + else + { + + GenericPoco poco = await Serializer.DeserializeWrapper>(wrappedJson, options); + Assert.Equal(expectedValue, poco.Property, equalityComparer); + } + } + + if (contexts.HasFlag(SerializedValueContext.CollectionElement)) + { + wrappedJson = $@"[{json}]"; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.DeserializeWrapper>(wrappedJson, options)); + } + else + { + List list = await Serializer.DeserializeWrapper>(wrappedJson, options); + Assert.Equal(1, list.Count); + Assert.Equal(expectedValue, list[0], equalityComparer); + } + } + + if (contexts.HasFlag(SerializedValueContext.DictionaryValue)) + { + const string key = "key"; + wrappedJson = $@"{{ ""{key}"" : {json} }}"; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, () => Serializer.DeserializeWrapper>(wrappedJson, options)); + } + else + { + Dictionary dictionary = await Serializer.DeserializeWrapper>(wrappedJson, options); + Assert.Equal(1, dictionary.Count); + Assert.True(dictionary.ContainsKey(key)); + Assert.Equal(expectedValue, dictionary[key], equalityComparer); + } + } + + if (contexts.HasFlag(SerializedValueContext.JsonNode)) + { + const string key = "key"; + wrappedJson = $@"{{ ""{key}"" : {json} }}"; + + if (expectedExceptionType != null) + { + await Assert.ThrowsAsync(expectedExceptionType, + async () => + { + JsonNode jsonNode = await Serializer.DeserializeWrapper(wrappedJson, options); + JsonSerializer.Deserialize(jsonNode[key], options); + }); + } + else + { + JsonNode jsonNode = await Serializer.DeserializeWrapper(wrappedJson, options); + TValue value = JsonSerializer.Deserialize(jsonNode[key], options); + Assert.Equal(expectedValue, value, equalityComparer); + } + } + } + + /// + /// Tests deserialization of a given list of values within the context of multiple types: + /// root values, object properties, collection elements, dictionary values, etc. + /// + protected async Task TestMultiContextDeserialization( + IEnumerable<(string Json, TValue ExpectedValue)> inputs, + SerializedValueContext contexts = SerializedValueContext.All, + JsonSerializerOptions? options = null, + IEqualityComparer? equalityComparer = null) + { + inputs = inputs.ToList(); + List expectedValues = inputs.Select(x => x.ExpectedValue).ToList(); + string json = $"[{string.Join(",", inputs.Select(x => x.Json))}]"; + var listEqualityComparer = new ListAssertionEqualityComparer(equalityComparer); + await TestMultiContextDeserialization>(json, expectedValues, expectedExceptionType: null, contexts, options, listEqualityComparer); + } + + private class GenericPoco + { + public T Property { get; set; } + } + + private class JsonEqualityComparer : IEqualityComparer + { + public bool Equals(TValue? x, TValue? y) => JsonSerializer.Serialize(x) == JsonSerializer.Serialize(y); + public int GetHashCode([DisallowNull] TValue obj) => JsonSerializer.Serialize(obj).GetHashCode(); + } + + private class ListAssertionEqualityComparer : IEqualityComparer> + { + private readonly IEqualityComparer? _elementComparer; + + public ListAssertionEqualityComparer(IEqualityComparer? elementComparer) + { + _elementComparer = elementComparer ?? EqualityComparer.Default; + } + + public bool Equals(IList? x, IList? y) + { + Assert.Equal(x, y, _elementComparer); + return true; + } + + public int GetHashCode([DisallowNull] IList obj) => throw new NotImplementedException(); } } } 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 a21ab179cc8eda..96184e67511951 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 @@ -49,6 +49,7 @@ public interface ITestContext public JsonTypeInfo NullablePersonStruct { get; } public JsonTypeInfo TypeWithValidationAttributes { get; } public JsonTypeInfo TypeWithDerivedAttribute { get; } + public JsonTypeInfo PolymorphicClass { get; } } internal partial class JsonContext : JsonSerializerContext 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 092d6110170739..03f11be1811803 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 @@ -43,6 +43,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(PersonStruct?))] [JsonSerializable(typeof(TypeWithValidationAttributes))] [JsonSerializable(typeof(TypeWithDerivedAttribute))] + [JsonSerializable(typeof(PolymorphicClass))] internal partial class MetadataAndSerializationContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Default; 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 0f9df146d25450..abd64426c0caa2 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 @@ -42,6 +42,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(TypeWithValidationAttributes), GenerationMode = JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Metadata)] + [JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Metadata)] internal partial class MetadataWithPerTypeAttributeContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata; @@ -130,6 +131,7 @@ public override void EnsureFastPathGeneratedAsExpected() [JsonSerializable(typeof(PersonStruct?))] [JsonSerializable(typeof(TypeWithValidationAttributes))] [JsonSerializable(typeof(TypeWithDerivedAttribute))] + [JsonSerializable(typeof(PolymorphicClass))] internal partial class MetadataContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata; 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 202a014f2d6077..a5867b9851744e 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 @@ -43,6 +43,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(TypeWithValidationAttributes), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization)] internal partial class MixedModeContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Metadata | JsonSourceGenerationMode.Serialization; 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 cfc6d486c7dd03..a2d2e1d0aa7285 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 @@ -925,5 +925,40 @@ public void TypeWithDerivedAttribute() instance = JsonSerializer.Deserialize(json, DefaultContext.TypeWithDerivedAttribute); Assert.NotNull(instance); } + + [Fact] + public void PolymorphicClass_Serialization() + { + PolymorphicClass value = new PolymorphicClass.DerivedClass { Number = 42, Boolean = true }; + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Serialize(value, DefaultContext.PolymorphicClass)); + } + else + { + string expectedJson = @"{""$type"" : ""derivedClass"", ""Number"" : 42, ""Boolean"" : true }"; + string actualJson = JsonSerializer.Serialize(value, DefaultContext.PolymorphicClass); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + } + + [Fact] + public void PolymorphicClass_Deserialization() + { + string json = @"{""$type"" : ""derivedClass"", ""Number"" : 42, ""Boolean"" : true }"; + + if (DefaultContext.JsonSourceGenerationMode == JsonSourceGenerationMode.Serialization) + { + Assert.Throws(() => JsonSerializer.Deserialize(json, DefaultContext.PolymorphicClass)); + } + else + { + PolymorphicClass result = JsonSerializer.Deserialize(json, DefaultContext.PolymorphicClass); + PolymorphicClass.DerivedClass derivedResult = Assert.IsType(result); + Assert.Equal(42, derivedResult.Number); + Assert.True(derivedResult.Boolean); + } + } } } 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 b177cfff8ea117..12192cd157b630 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 @@ -43,6 +43,7 @@ namespace System.Text.Json.SourceGeneration.Tests [JsonSerializable(typeof(PersonStruct?))] [JsonSerializable(typeof(TypeWithValidationAttributes))] [JsonSerializable(typeof(TypeWithDerivedAttribute))] + [JsonSerializable(typeof(PolymorphicClass))] internal partial class SerializationContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization; @@ -84,6 +85,7 @@ internal partial class SerializationContext : JsonSerializerContext, ITestContex [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(TypeWithValidationAttributes), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Serialization)] internal partial class SerializationWithPerTypeAttributeContext : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization; @@ -126,6 +128,7 @@ internal partial class SerializationWithPerTypeAttributeContext : JsonSerializer [JsonSerializable(typeof(PersonStruct?), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(TypeWithValidationAttributes), GenerationMode = JsonSourceGenerationMode.Serialization)] [JsonSerializable(typeof(TypeWithDerivedAttribute), GenerationMode = JsonSourceGenerationMode.Serialization)] + [JsonSerializable(typeof(PolymorphicClass), GenerationMode = JsonSourceGenerationMode.Serialization)] internal partial class SerializationContextWithCamelCase : JsonSerializerContext, ITestContext { public JsonSourceGenerationMode JsonSourceGenerationMode => JsonSourceGenerationMode.Serialization; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index ccd7e654723a91..68f0256880f145 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -6,13 +6,18 @@ - $(NoWarn);SYSLIB0020;SYSLIB1037;SYSLIB1038 + + $(NoWarn);SYSLIB0020;SYSLIB1037;SYSLIB1038;SYSLIB1039 $(DefineConstants);BUILDING_SOURCE_GENERATOR_TESTS + + + + 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 a9124235863371..0aec96e1b47831 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 @@ -180,4 +180,15 @@ public class DerivedAttribute : BaseAttribute [Derived(TestProperty = "Test")] public class TypeWithDerivedAttribute { } + + [JsonDerivedType(typeof(DerivedClass), "derivedClass")] + public class PolymorphicClass + { + public int Number { get; set; } + + public class DerivedClass : PolymorphicClass + { + public bool Boolean { get; set; } + } + } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonPolymorphicTypeConfigurationTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonPolymorphicTypeConfigurationTests.cs new file mode 100644 index 00000000000000..0c37dd0d7c5c33 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonPolymorphicTypeConfigurationTests.cs @@ -0,0 +1,158 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json.Serialization; +using Xunit; + +namespace System.Text.Json.Tests.Serialization +{ + public static class JsonPolymorphicTypeConfigurationTests + { + [Theory] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(Interface))] + [InlineData(typeof(Class))] + [InlineData(typeof(GenericClass))] + public static void SupportedBaseTypeArgument_ShouldSucced(Type baseType) + { + var configuration = new JsonPolymorphicTypeConfiguration(baseType); + Assert.Equal(baseType, configuration.BaseType); + Assert.Empty(configuration); + } + + [Theory] + [InlineData(typeof(IEnumerable), typeof(IEnumerable))] + [InlineData(typeof(IList), typeof(string[]))] + [InlineData(typeof(MemberInfo), typeof(Type))] + [InlineData(typeof(Interface), typeof(Class))] + [InlineData(typeof(Interface), typeof(Struct))] + [InlineData(typeof(Class), typeof(GenericClass))] + public static void SupportedDerivedTypeArgument_ShouldSucced(Type baseType, Type derivedType) + { + var configuration = new JsonPolymorphicTypeConfiguration(baseType).WithDerivedType(derivedType); + Assert.Equal(new[] { (derivedType, (string)null) }, configuration); + + configuration = new JsonPolymorphicTypeConfiguration(baseType).WithDerivedType(derivedType, "typeDiscriminator"); + Assert.Equal(new[] { (derivedType, "typeDiscriminator") }, configuration); + } + + [Fact] + public static void SupportsDeclaringBaseTypeAsDerivedType() + { + var configuration = + new JsonPolymorphicTypeConfiguration(typeof(Class)) + .WithDerivedType(typeof(Class)); + + Assert.Equal(new[] { (typeof(Class), (string)null) }, configuration); + + configuration = + new JsonPolymorphicTypeConfiguration(typeof(Class)) + .WithDerivedType(typeof(Class), "typeDiscriminatorId"); + + Assert.Equal(new[] { (typeof(Class), "typeDiscriminatorId") }, configuration); + } + + [Fact] + public static void SupportsMixingAndMatchingTypeDiscriminators() + { + var configuration = + new JsonPolymorphicTypeConfiguration(typeof(Class)) + .WithDerivedType(typeof(GenericClass), "typeDiscriminator") + .WithDerivedType(typeof(GenericClass)); + + Assert.Equal( + new[] { (typeof(GenericClass), "typeDiscriminator"), (typeof(GenericClass), (string)null) }, + configuration); + } + + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(int*))] + [InlineData(typeof(string))] + [InlineData(typeof(object))] + [InlineData(typeof(Guid))] + [InlineData(typeof(Struct))] + [InlineData(typeof(ReadOnlySpan))] + [InlineData(typeof(SealedClass))] + [InlineData(typeof(GenericClass<>))] + public static void InvalidBaseTypeArgument_ThrowsArgumentException(Type baseType) + { + Assert.Throws(() => new JsonPolymorphicTypeConfiguration(baseType)); + } + + [Fact] + public static void NullBaseTypeArgument_ThrowsArgumentNullException() + { + Assert.Throws(() => new JsonPolymorphicTypeConfiguration(null)); + } + + [Theory] + [InlineData(typeof(Interface), typeof(object))] + [InlineData(typeof(Class), typeof(Interface))] + [InlineData(typeof(Class), typeof(GenericClass<>))] + public static void InvalidDerivedTypeArgument_ThrowsArgumentException(Type baseType, Type derivedType) + { + var configuration = new JsonPolymorphicTypeConfiguration(baseType); + + Assert.Throws(() => configuration.WithDerivedType(derivedType)); + Assert.Empty(configuration); + + Assert.Throws(() => configuration.WithDerivedType(derivedType, "typeDiscriminator")); + Assert.Empty(configuration); + } + + [Fact] + public static void NullDerivedTypeArgument_ThrowsArgumentNullException() + { + var configuration = new JsonPolymorphicTypeConfiguration(typeof(Class)); + Assert.Throws(() => configuration.WithDerivedType(derivedType: null)); + Assert.Empty(configuration); + } + + [Fact] + public static void DuplicateDerivedType_ThrowsArgumentException() + { + var configuration = new JsonPolymorphicTypeConfiguration(typeof(Class)).WithDerivedType(typeof(GenericClass)); + Assert.Throws(() => configuration.WithDerivedType(typeof(GenericClass))); + Assert.Equal(new[] { (typeof(GenericClass), (string)null) }, configuration); + } + + [Fact] + public static void DuplicateTypeDiscriminator_ThrowsArgumentException() + { + var configuration = + new JsonPolymorphicTypeConfiguration(typeof(Class)) + .WithDerivedType(typeof(GenericClass), "discriminator1") + .WithDerivedType(typeof(GenericClass), "discriminator2"); + + Assert.Equal(new[] { (typeof(GenericClass), "discriminator1"), (typeof(GenericClass), "discriminator2") }, configuration); + + Assert.Throws(() => configuration.WithDerivedType(typeof(GenericClass), "discriminator2")); + + Assert.Equal(new[] { (typeof(GenericClass), "discriminator1"), (typeof(GenericClass), "discriminator2") }, configuration); + } + + [Fact] + public static void ModifyingAfterAssignmentToOptions_ShouldThrowInvalidOperationException() + { + var config = new JsonPolymorphicTypeConfiguration(typeof(Class)) + .WithDerivedType(typeof(GenericClass), "derived"); + + _ = new JsonSerializerOptions { PolymorphicTypeConfigurations = { config } }; + + Assert.Throws(() => config.WithDerivedType(typeof(GenericClass), "derived2")); + Assert.Throws(() => config.CustomTypeDiscriminatorPropertyName = "_case"); + Assert.Throws(() => config.UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToBaseType); + Assert.Throws(() => config.IgnoreUnrecognizedTypeDiscriminators = true); + } + + private interface Interface { } + private class Class : Interface { } + private struct Struct : Interface { } + private sealed class SealedClass : Interface { } + private class GenericClass : Class { } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs index 791fe69b99f183..064f5d9c948948 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/OptionsTests.cs @@ -587,6 +587,13 @@ private static JsonSerializerOptions GetFullyPopulatedOptionsInstance() options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new ConverterForInt32()); } + else if (propertyType == typeof(IList)) + { + options.PolymorphicTypeConfigurations.Add( + new JsonPolymorphicTypeConfiguration() + .WithDerivedType("point_with_array") + .WithDerivedType("point_with_dictionary")); + } else if (propertyType == typeof(JavaScriptEncoder)) { options.Encoder = JavaScriptEncoder.Default; @@ -632,7 +639,7 @@ private static void VerifyOptionsEqual(JsonSerializerOptions options, JsonSerial { Assert.Equal((int)property.GetValue(options), (int)property.GetValue(newOptions)); } - else if (typeof(IEnumerable).IsAssignableFrom(propertyType)) + else if (propertyType == typeof(IList)) { var list1 = (IList)property.GetValue(options); var list2 = (IList)property.GetValue(newOptions); @@ -643,6 +650,17 @@ private static void VerifyOptionsEqual(JsonSerializerOptions options, JsonSerial Assert.Same(list1[i], list2[i]); } } + else if (propertyType == typeof(IList)) + { + var list1 = (IList)property.GetValue(options); + var list2 = (IList)property.GetValue(newOptions); + + Assert.Equal(list1.Count, list2.Count); + for (int i = 0; i < list1.Count; i++) + { + Assert.Same(list1[i], list2[i]); + } + } else if (propertyType.IsValueType) { if (property.Name == "ReadCommentHandling") diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs new file mode 100644 index 00000000000000..d6ab311a564a69 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.CustomTypeHierarchies.cs @@ -0,0 +1,2438 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public abstract partial class PolymorphicTests + { + #region Polymorphic Class + [Theory] + [MemberData(nameof(Get_PolymorphicClass_TestData_Serialization))] + public Task PolymorphicClass_TestData_Serialization(PolymorphicClass.TestData testData) + => TestMultiContextSerialization(testData.Value, testData.ExpectedJson, testData.ExpectedSerializationException); + + public static IEnumerable Get_PolymorphicClass_TestData_Serialization() + => PolymorphicClass.GetSerializeTestData().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicClass_TestData_Deserialization))] + public Task PolymorphicClass_TestData_Deserialization(PolymorphicClass.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + testData.ExpectedDeserializationException, + equalityComparer: PolymorphicEqualityComparer.Instance); + + public static IEnumerable Get_PolymorphicClass_TestData_Deserialization() + => PolymorphicClass.GetSerializeTestData().Where(entry => entry.ExpectedJson != null).Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicClass_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicClass Value, string ExpectedJson)> inputs = + PolymorphicClass.GetSerializeTestData() + .Where(entry => entry.ExpectedSerializationException is null) + .Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs); + } + + [Fact] + public async Task PolymorphicClass_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicClass ExpectedRoundtripValue)> inputs = + PolymorphicClass.GetSerializeTestData() + .Where(entry => entry.ExpectedRoundtripValue is not null) + .Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization(inputs, equalityComparer: PolymorphicEqualityComparer.Instance); + } + + [Theory] + [InlineData("$.$type", @"{ ""$type"" : ""derivedClass1"", ""$type"" : ""derivedClass1"", ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : ""derivedClass1"", ""Number"" : 42, ""$type"" : ""derivedClass1""}")] + [InlineData("$.$id", @"{ ""$type"" : ""derivedClass1"", ""Number"" : 42, ""$id"" : ""referenceId""}")] + [InlineData("$.$id", @"{ ""$type"" : ""derivedClass1"", """" : 42, ""$id"" : ""referenceId""}")] + [InlineData("$.$values", @"{ ""Number"" : 42, ""$values"" : [] }")] + [InlineData("$.$type", @"{ ""Number"" : 42, ""$type"" : ""derivedClass"" }")] + [InlineData("$", @"{ ""$type"" : ""invalidDiscriminator"", ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : 0, ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : false, ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : {}, ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : [], ""Number"" : 42 }")] + [InlineData("$.$id", @"{ ""$id"" : ""1"", ""Number"" : 42 }")] + [InlineData("$.$ref", @"{ ""$ref"" : ""1"" }")] + public async Task PolymorphicClass_InvalidTypeDiscriminatorMetadata_ShouldThrowJsonException(string expectedJsonPath, string json) + { + JsonException exception = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Equal(expectedJsonPath, exception.Path); + } + + //-- + + [Theory] + [MemberData(nameof(Get_PolymorphicClass_CustomConfigWithBaseTypeFallback_TestData_Serialization))] + public Task PolymorphicClass_CustomConfigWithBaseTypeFallback_TestData_Serialization(PolymorphicClass.TestData testData) + => TestMultiContextSerialization( + testData.Value, + testData.ExpectedJson, + testData.ExpectedSerializationException, + options: PolymorphicClass.CustomConfigWithBaseTypeFallback); + + public static IEnumerable Get_PolymorphicClass_CustomConfigWithBaseTypeFallback_TestData_Serialization() + => PolymorphicClass.GetSerializeTestData_CustomConfigWithBaseTypeFallback().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicClass_CustomConfigWithBaseTypeFallback_TestData_Deserialization))] + public Task PolymorphicClass_CustomConfigWithBaseTypeFallback_TestData_Deserialization(PolymorphicClass.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + testData.ExpectedSerializationException, + equalityComparer: PolymorphicEqualityComparer.Instance, + options: PolymorphicClass.CustomConfigWithBaseTypeFallback); + + public static IEnumerable Get_PolymorphicClass_CustomConfigWithBaseTypeFallback_TestData_Deserialization() + => PolymorphicClass.GetSerializeTestData_CustomConfigWithBaseTypeFallback() + .Where(entry => entry.ExpectedJson != null) + .Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicClass_CustomConfigWithBaseTypeFallback_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicClass Value, string ExpectedJson)> inputs = + PolymorphicClass.GetSerializeTestData_CustomConfigWithBaseTypeFallback() + .Where(entry => entry.ExpectedSerializationException is null) + .Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs, options: PolymorphicClass.CustomConfigWithBaseTypeFallback); + } + + [Fact] + public async Task PolymorphicClass_CustomConfigWithBaseTypeFallbacks_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicClass ExpectedRoundtripValue)> inputs = + PolymorphicClass.GetSerializeTestData_CustomConfigWithBaseTypeFallback() + .Where(entry => entry.ExpectedRoundtripValue is not null) + .Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization( + inputs, + equalityComparer: PolymorphicEqualityComparer.Instance, + options: PolymorphicClass.CustomConfigWithBaseTypeFallback); + } + + [Theory] + [InlineData("$.$type", @"{ ""$type"" : ""derivedClass1"", ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : ""derivedClass1"", ""_case"" : ""derivedClass1"", ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""_case"" : ""derivedClass1""}")] + [InlineData("$.$type", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""$type"" : ""derivedClass1""}")] + [InlineData("$.$id", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""$id"" : ""referenceId""}")] + [InlineData("$.$id", @"{ ""_case"" : ""derivedClass1"", """" : 42, ""$id"" : ""referenceId""}")] + [InlineData("$.$values", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""$values"" : [] }")] + [InlineData("$._case", @"{ ""Number"" : 42, ""_case"" : ""derivedClass1"" }")] + [InlineData("$", @"{ ""_case"" : ""invalidDiscriminator"", ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : 0, ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : false, ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : {}, ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : [], ""Number"" : 42 }")] + [InlineData("$.$id", @"{ ""$id"" : ""1"", ""Number"" : 42 }")] + [InlineData("$.$ref", @"{ ""$ref"" : ""1"" }")] + public async Task PolymorphicClass_CustomConfigWithBaseTypeFallback_InvalidTypeDiscriminatorMetadata_ShouldThrowJsonException(string expectedJsonPath, string json) + { + JsonException exception = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, PolymorphicClass.CustomConfigWithBaseTypeFallback)); + Assert.Equal(expectedJsonPath, exception.Path); + } + + //--- + + [Theory] + [MemberData(nameof(Get_PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestData_Serialization))] + public Task PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestData_Serialization(PolymorphicClass.TestData testData) + => TestMultiContextSerialization( + testData.Value, + testData.ExpectedJson, + testData.ExpectedSerializationException, + options: PolymorphicClass.CustomConfigWithNearestAncestorFallback); + + public static IEnumerable Get_PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestData_Serialization() + => PolymorphicClass.GetSerializeTestData_CustomConfigWithNearestAncestorFallback().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestData_Deserialization))] + public Task PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestData_Deserialization(PolymorphicClass.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + testData.ExpectedDeserializationException, + equalityComparer: PolymorphicEqualityComparer.Instance, + options: PolymorphicClass.CustomConfigWithNearestAncestorFallback); + + public static IEnumerable Get_PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestData_Deserialization() + => PolymorphicClass.GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + .Where(entry => entry.ExpectedJson != null) + .Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicClass Value, string ExpectedJson)> inputs = + PolymorphicClass.GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + .Where(entry => entry.ExpectedSerializationException is null) + .Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs, options: PolymorphicClass.CustomConfigWithNearestAncestorFallback); + } + + [Fact] + public async Task PolymorphicClass_CustomConfigWithNearestAncestorFallback_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicClass ExpectedRoundtripValue)> inputs = + PolymorphicClass.GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + .Where(entry => entry.ExpectedRoundtripValue is not null) + .Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization( + inputs, + equalityComparer: PolymorphicEqualityComparer.Instance, + options: PolymorphicClass.CustomConfigWithNearestAncestorFallback); + } + + [Theory] + [InlineData("$.$type", @"{ ""$type"" : ""derivedClass1"", ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : ""derivedClass1"", ""_case"" : ""derivedClass1"", ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""_case"" : ""derivedClass1""}")] + [InlineData("$.$type", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""$type"" : ""derivedClass1""}")] + [InlineData("$.$id", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""$id"" : ""referenceId""}")] + [InlineData("$.$id", @"{ ""_case"" : ""derivedClass1"", """" : 42, ""$id"" : ""referenceId""}")] + [InlineData("$.$values", @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""$values"" : [] }")] + [InlineData("$._case", @"{ ""Number"" : 42, ""_case"" : ""derivedClass1"" }")] + [InlineData("$", @"{ ""_case"" : ""invalidDiscriminator"", ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : 0, ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : false, ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : {}, ""Number"" : 42 }")] + [InlineData("$._case", @"{ ""_case"" : [], ""Number"" : 42 }")] + [InlineData("$.$id", @"{ ""$id"" : ""1"", ""Number"" : 42 }")] + [InlineData("$.$ref", @"{ ""$ref"" : ""1"" }")] + public async Task PolymorphicClass_CustomConfigWithNearestAncestorFallback_InvalidTypeDiscriminatorMetadata_ShouldThrowJsonException(string expectedJsonPath, string json) + { + JsonException exception = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, PolymorphicClass.CustomConfigWithBaseTypeFallback)); + Assert.Equal(expectedJsonPath, exception.Path); + } + + [Theory] + [InlineData(JsonUnknownDerivedTypeHandling.FailSerialization)] + [InlineData(JsonUnknownDerivedTypeHandling.FallbackToBaseType)] + public async Task PolymorphicClass_ConfigWithAbstractClass_ShouldThrowNotSupportedException(JsonUnknownDerivedTypeHandling jsonUnknownDerivedTypeHandling) + { + var options = new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration { UnknownDerivedTypeHandling = jsonUnknownDerivedTypeHandling } + .WithDerivedType() + } + }; + + PolymorphicClass value = new PolymorphicClass.DerivedAbstractClass.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(DerivedClass1_NoTypeDiscriminator))] + [JsonDerivedType(typeof(DerivedClass1_NoTypeDiscriminator.DerivedClass))] + [JsonDerivedType(typeof(DerivedClass1_TypeDiscriminator), "derivedClass1")] + [JsonDerivedType(typeof(DerivedClass1_TypeDiscriminator.DerivedClass), "derivedClassOfDerivedClass1")] + [JsonDerivedType(typeof(DerivedClass2_NoTypeDiscriminator))] + [JsonDerivedType(typeof(DerivedClass2_TypeDiscriminator), "derivedClass2")] + [JsonDerivedType(typeof(DerivedCollection_NoTypeDiscriminator))] + [JsonDerivedType(typeof(DerivedCollection_TypeDiscriminator), "derivedCollection")] + [JsonDerivedType(typeof(DerivedCollection_TypeDiscriminator.DerivedClass), "derivedCollectionOfDerivedCollection")] + [JsonDerivedType(typeof(DerivedDictionary_NoTypeDiscriminator))] + [JsonDerivedType(typeof(DerivedDictionary_TypeDiscriminator), "derivedDictionary")] + [JsonDerivedType(typeof(DerivedDictionary_TypeDiscriminator.DerivedClass), "derivedDictionaryOfDerivedDictionary")] + [JsonDerivedType(typeof(DerivedClassWithConstructor_TypeDiscriminator), "derivedClassWithCtor")] + [JsonDerivedType(typeof(DerivedClassWithConstructor_TypeDiscriminator.DerivedClass), "derivedClassOfDerivedClassWithCtor")] + [JsonDerivedType(typeof(DerivedClassWithCustomConverter_NoTypeDiscriminator))] + [JsonDerivedType(typeof(DerivedClassWithCustomConverter_TypeDiscriminator), "derivedClassWithCustomConverter")] + public class PolymorphicClass + { + public int Number { get; set; } + + public class DerivedClass1_NoTypeDiscriminator : PolymorphicClass + { + public string String { get; set; } + + public class DerivedClass : DerivedClass1_NoTypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + // Ensure derived class polymorphic configuration is not inherited by base class polymorphic configuration + [JsonPolymorphic(CustomTypeDiscriminatorPropertyName = "$case")] + [JsonDerivedType(typeof(DerivedClass), "derivedClassOfDerivedClass1")] + public class DerivedClass1_TypeDiscriminator : PolymorphicClass + { + public string String { get; set; } + + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } + + public class DerivedClass : DerivedClass1_TypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public class DerivedClass2_NoTypeDiscriminator : PolymorphicClass + { + public bool Boolean { get; set; } + + public class DerivedClass : DerivedClass2_NoTypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public class DerivedClass2_TypeDiscriminator : PolymorphicClass + { + public bool Boolean { get; set; } + + public class DerivedClass : DerivedClass2_TypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public abstract class DerivedAbstractClass : PolymorphicClass + { + public abstract bool Boolean { get; set; } + + public class DerivedClass : DerivedAbstractClass + { + public override bool Boolean { get; set; } + } + } + + public class DerivedCollection_NoTypeDiscriminator : PolymorphicClass, ICollection + { + // Minimal ICollection implementation meant to enable collection deserialization + bool ICollection.IsReadOnly => false; + void ICollection.Add(int item) => Number = item; + public IEnumerator GetEnumerator() => Enumerable.Repeat(Number, 3).GetEnumerator(); + + int ICollection.Count => throw new NotImplementedException(); + void ICollection.Clear() => throw new NotImplementedException(); + bool ICollection.Contains(int item) => throw new NotImplementedException(); + void ICollection.CopyTo(int[] array, int arrayIndex) => throw new NotImplementedException(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + bool ICollection.Remove(int item) => throw new NotImplementedException(); + + public class DerivedClass : DerivedCollection_NoTypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public class DerivedCollection_TypeDiscriminator : PolymorphicClass, ICollection + { + // Minimal ICollection implementation meant to enable collection deserialization + bool ICollection.IsReadOnly => false; + void ICollection.Add(int item) => Number = item; + public IEnumerator GetEnumerator() => Enumerable.Repeat(Number, 3).GetEnumerator(); + + int ICollection.Count => throw new NotImplementedException(); + void ICollection.Clear() => throw new NotImplementedException(); + bool ICollection.Contains(int item) => throw new NotImplementedException(); + void ICollection.CopyTo(int[] array, int arrayIndex) => throw new NotImplementedException(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + bool ICollection.Remove(int item) => throw new NotImplementedException(); + + public class DerivedClass : DerivedCollection_TypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public class DerivedDictionary_NoTypeDiscriminator : PolymorphicClass, IDictionary + { + // Minimal IDictionary implementation meant to enable serialization + bool ICollection>.IsReadOnly => false; + public IEnumerator> GetEnumerator() => Enumerable.Repeat(new KeyValuePair("dictionaryKey", Number), 1).GetEnumerator(); + int IDictionary.this[string key] { get => throw new NotImplementedException(); set { if (key == "dictionaryKey") Number = value; } } + + void IDictionary.Add(string key, int value) => throw new NotImplementedException(); + ICollection IDictionary.Keys => throw new NotImplementedException(); + ICollection IDictionary.Values => throw new NotImplementedException(); + int ICollection>.Count => throw new NotImplementedException(); + void ICollection>.Add(KeyValuePair item) => throw new NotImplementedException(); + void ICollection>.Clear() => throw new NotImplementedException(); + bool ICollection>.Contains(KeyValuePair item) => throw new NotImplementedException(); + bool IDictionary.ContainsKey(string key) => throw new NotImplementedException(); + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotImplementedException(); + IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); + bool IDictionary.Remove(string key) => throw new NotImplementedException(); + bool ICollection>.Remove(KeyValuePair item) => throw new NotImplementedException(); + bool IDictionary.TryGetValue(string key, out int value) => throw new NotImplementedException(); + + public class DerivedClass : DerivedDictionary_NoTypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public class DerivedDictionary_TypeDiscriminator : PolymorphicClass, IDictionary + { + // Minimal IDictionary implementation meant to enable serialization + bool ICollection>.IsReadOnly => false; + public IEnumerator> GetEnumerator() => Enumerable.Repeat(new KeyValuePair("dictionaryKey", Number), 1).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + int IDictionary.this[string key] { get => throw new NotImplementedException(); set { if (key == "dictionaryKey") Number = value; } } + + void IDictionary.Add(string key, int value) => throw new NotImplementedException(); + ICollection IDictionary.Keys => throw new NotImplementedException(); + ICollection IDictionary.Values => throw new NotImplementedException(); + int ICollection>.Count => throw new NotImplementedException(); + void ICollection>.Add(KeyValuePair item) => throw new NotImplementedException(); + void ICollection>.Clear() => throw new NotImplementedException(); + bool ICollection>.Contains(KeyValuePair item) => throw new NotImplementedException(); + bool IDictionary.ContainsKey(string key) => throw new NotImplementedException(); + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotImplementedException(); + bool IDictionary.Remove(string key) => throw new NotImplementedException(); + bool ICollection>.Remove(KeyValuePair item) => throw new NotImplementedException(); + bool IDictionary.TryGetValue(string key, out int value) => throw new NotImplementedException(); + + public class DerivedClass : DerivedDictionary_TypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public class DerivedClassWithConstructor_TypeDiscriminator : PolymorphicClass + { + [JsonConstructor] + public DerivedClassWithConstructor_TypeDiscriminator(int number) + { + Number = number; + } + + public class DerivedClass : DerivedClassWithConstructor_TypeDiscriminator + { + [JsonConstructor] + public DerivedClass(int number, string extraProperty) + : base(number) + { + ExtraProperty = extraProperty; + } + + public string ExtraProperty { get; set; } + } + } + + [JsonConverter(typeof(CustomConverter))] + public class DerivedClassWithCustomConverter_NoTypeDiscriminator : PolymorphicClass + { + public class CustomConverter : JsonConverter + { + public override DerivedClassWithCustomConverter_NoTypeDiscriminator Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => throw new NotSupportedException(); + + public override void Write(Utf8JsonWriter writer, DerivedClassWithCustomConverter_NoTypeDiscriminator value, JsonSerializerOptions options) + => writer.WriteNumberValue(value.Number); + } + + public class DerivedClass : DerivedClassWithCustomConverter_NoTypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + [JsonConverter(typeof(CustomConverter))] + public class DerivedClassWithCustomConverter_TypeDiscriminator : PolymorphicClass + { + public class CustomConverter : JsonConverter + { + public override DerivedClassWithCustomConverter_TypeDiscriminator Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => throw new NotSupportedException(); + + public override void Write(Utf8JsonWriter writer, DerivedClassWithCustomConverter_TypeDiscriminator value, JsonSerializerOptions options) + => writer.WriteNumberValue(value.Number); + } + + public class DerivedClass : DerivedClassWithCustomConverter_TypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public static IEnumerable GetSerializeTestData() + { + yield return new TestData( + Value: new PolymorphicClass { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_NoTypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass2_NoTypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedClass1_NoTypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"", ""ExtraProperty"" : ""extra"" }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_TypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""$type"" : ""derivedClass1"", ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedClass1_TypeDiscriminator { Number = 42, String = "str" }); + + yield return new TestData( + Value: new DerivedClass1_TypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }, + ExpectedJson: @"{ ""$type"" : ""derivedClassOfDerivedClass1"", ""Number"" : 42, ""String"" : ""str"", ""ExtraProperty"" : ""extra"" }", + ExpectedRoundtripValue: new DerivedClass1_TypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }); + + yield return new TestData( + Value: new DerivedClass2_NoTypeDiscriminator { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""Number"" : 42, ""Boolean"" : true }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass2_TypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedClass2_TypeDiscriminator { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""$type"" : ""derivedClass2"", ""Number"" : 42, ""Boolean"" : true }", + ExpectedRoundtripValue: new DerivedClass2_TypeDiscriminator { Number = 42, Boolean = true }); + + yield return new TestData( + Value: new DerivedCollection_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"[42,42,42]", + ExpectedDeserializationException: typeof(JsonException)); + + yield return new TestData( + Value: new DerivedCollection_NoTypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedCollection_TypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""$type"" : ""derivedCollection"", ""$values"" : [42,42,42] }", + ExpectedRoundtripValue: new DerivedCollection_TypeDiscriminator { Number = 42 }); + + yield return new TestData( + Value: new DerivedCollection_TypeDiscriminator.DerivedClass { Number = 42, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""$type"" : ""derivedCollectionOfDerivedCollection"", ""$values"" : [42,42,42] }", + ExpectedRoundtripValue: new DerivedCollection_TypeDiscriminator.DerivedClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedDictionary_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""dictionaryKey"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass()); + + yield return new TestData( + Value: new DerivedDictionary_TypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""$type"":""derivedDictionary"", ""dictionaryKey"" : 42 }", + ExpectedRoundtripValue: new DerivedDictionary_TypeDiscriminator { Number = 42 }); + + yield return new TestData( + Value: new DerivedDictionary_TypeDiscriminator.DerivedClass { Number = 42, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""$type"" : ""derivedDictionaryOfDerivedDictionary"", ""dictionaryKey"" : 42 }", + ExpectedRoundtripValue: new DerivedDictionary_TypeDiscriminator.DerivedClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithConstructor_TypeDiscriminator(42), + ExpectedJson: @"{ ""$type"" : ""derivedClassWithCtor"", ""Number"" : 42 }", + ExpectedRoundtripValue: new DerivedClassWithConstructor_TypeDiscriminator(42)); + + yield return new TestData( + Value: new DerivedClassWithConstructor_TypeDiscriminator.DerivedClass(42, "extra"), + ExpectedJson: @"{ ""$type"" : ""derivedClassOfDerivedClassWithCtor"", ""Number"" : 42, ""ExtraProperty"" : ""extra"" }", + ExpectedRoundtripValue: new DerivedClassWithConstructor_TypeDiscriminator.DerivedClass(42, "extra")); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: "42", + ExpectedDeserializationException: typeof(JsonException)); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_NoTypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_TypeDiscriminator(), // TODO special unit test for type discriminators with custom converters + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_TypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + } + + public static JsonSerializerOptions CustomConfigWithBaseTypeFallback { get; } = + new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration + { + CustomTypeDiscriminatorPropertyName = "_case", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToBaseType + } + .WithDerivedType() + .WithDerivedType("derivedClass1") + .WithDerivedType("derivedClassOfDerivedClass1") + .WithDerivedType("derivedClass2") + .WithDerivedType("derivedCollection") + .WithDerivedType() + .WithDerivedType("derivedDictionaryOfDerivedDictionary") + .WithDerivedType("derivedClassOfDerivedClassWithCtor") + .WithDerivedType("derivedClassWithCustomConverter") + } + }; + + public static IEnumerable GetSerializeTestData_CustomConfigWithBaseTypeFallback() + { + yield return new TestData( + Value: new PolymorphicClass { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_NoTypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_NoTypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_TypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedClass1_TypeDiscriminator { Number = 42, String = "str" }); + + yield return new TestData( + Value: new DerivedClass1_TypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }, + ExpectedJson: @"{ ""_case"" : ""derivedClassOfDerivedClass1"", ""Number"" : 42, ""String"" : ""str"", ""ExtraProperty"" : ""extra"" }", + ExpectedRoundtripValue: new DerivedClass1_TypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }); + + yield return new TestData( + Value: new DerivedClass2_NoTypeDiscriminator { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass2_NoTypeDiscriminator.DerivedClass { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass2_TypeDiscriminator { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""_case"" : ""derivedClass2"", ""Number"" : 42, ""Boolean"" : true }", + ExpectedRoundtripValue: new DerivedClass2_TypeDiscriminator { Number = 42, Boolean = true }); + + yield return new TestData( + Value: new DerivedClass2_TypeDiscriminator.DerivedClass { Number = 42, Boolean = true, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedCollection_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedCollection_TypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""_case"" : ""derivedCollection"", ""$values"" : [42,42,42] }", + ExpectedRoundtripValue: new DerivedCollection_TypeDiscriminator { Number = 42 }); + + yield return new TestData( + Value: new DerivedCollection_TypeDiscriminator.DerivedClass { Number = 42, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedDictionary_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""dictionaryKey"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass()); + + yield return new TestData( + Value: new DerivedDictionary_TypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedDictionary_TypeDiscriminator.DerivedClass { Number = 42, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""_case"" : ""derivedDictionaryOfDerivedDictionary"", ""dictionaryKey"" : 42 }", + ExpectedRoundtripValue: new DerivedDictionary_TypeDiscriminator.DerivedClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithConstructor_TypeDiscriminator(42), + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithConstructor_TypeDiscriminator.DerivedClass(42, "extra"), + ExpectedJson: @"{ ""_case"" : ""derivedClassOfDerivedClassWithCtor"", ""Number"" : 42, ""ExtraProperty"" : ""extra"" }", + ExpectedRoundtripValue: new DerivedClassWithConstructor_TypeDiscriminator.DerivedClass(42, "extra")); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_TypeDiscriminator.DerivedClass { Number = 42, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_TypeDiscriminator(), + ExpectedSerializationException: typeof(NotSupportedException)); + } + + public static JsonSerializerOptions CustomConfigWithNearestAncestorFallback { get; } = + new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration + { + CustomTypeDiscriminatorPropertyName = "_case", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor + } + .WithDerivedType() + .WithDerivedType("derivedClass1") + .WithDerivedType("derivedClassOfDerivedClass1") + .WithDerivedType("derivedClass2") + .WithDerivedType("derivedAbstractClass") + .WithDerivedType("derivedCollection") + .WithDerivedType() + .WithDerivedType("derivedDictionaryOfDerivedDictionary") + .WithDerivedType("derivedClassOfDerivedClassWithCtor") + .WithDerivedType("derivedClassWithCustomConverter") + } + }; + + public static IEnumerable GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + { + yield return new TestData( + Value: new PolymorphicClass { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_NoTypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_NoTypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass1_TypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""_case"" : ""derivedClass1"", ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedClass1_TypeDiscriminator { Number = 42, String = "str" }); + + yield return new TestData( + Value: new DerivedClass1_TypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }, + ExpectedJson: @"{ ""_case"" : ""derivedClassOfDerivedClass1"", ""Number"" : 42, ""String"" : ""str"", ""ExtraProperty"" : ""extra"" }", + ExpectedRoundtripValue: new DerivedClass1_TypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }); + + yield return new TestData( + Value: new DerivedClass2_NoTypeDiscriminator { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass2_NoTypeDiscriminator.DerivedClass { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClass2_TypeDiscriminator { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""_case"" : ""derivedClass2"", ""Number"" : 42, ""Boolean"" : true }", + ExpectedRoundtripValue: new DerivedClass2_TypeDiscriminator { Number = 42, Boolean = true }); + + yield return new TestData( + Value: new DerivedClass2_TypeDiscriminator.DerivedClass { Number = 42, Boolean = true, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""_case"" : ""derivedClass2"", ""Number"" : 42, ""Boolean"" : true }", + ExpectedRoundtripValue: new DerivedClass2_TypeDiscriminator { Number = 42, Boolean = true }); + + yield return new TestData( + Value: new DerivedAbstractClass.DerivedClass { Number = 42, Boolean = true }, + ExpectedJson: @"{ ""_case"" : ""derivedAbstractClass"", ""Number"" : 42, ""Boolean"" : true }", + ExpectedDeserializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedCollection_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedCollection_TypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""_case"" : ""derivedCollection"", ""$values"" : [42,42,42] }", + ExpectedRoundtripValue: new DerivedCollection_TypeDiscriminator { Number = 42 }); + + yield return new TestData( + Value: new DerivedCollection_TypeDiscriminator.DerivedClass { Number = 42, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""_case"" : ""derivedCollection"", ""$values"" : [42,42,42] }", + ExpectedRoundtripValue: new DerivedCollection_TypeDiscriminator { Number = 42 }); + + yield return new TestData( + Value: new DerivedDictionary_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""dictionaryKey"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass()); + + yield return new TestData( + Value: new DerivedDictionary_TypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedDictionary_TypeDiscriminator.DerivedClass { Number = 42, ExtraProperty = "extra" }, + ExpectedJson: @"{ ""_case"" : ""derivedDictionaryOfDerivedDictionary"", ""dictionaryKey"" : 42 }", + ExpectedRoundtripValue: new DerivedDictionary_TypeDiscriminator.DerivedClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithConstructor_TypeDiscriminator(42), + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithConstructor_TypeDiscriminator.DerivedClass(42, "extra"), + ExpectedJson: @"{ ""_case"" : ""derivedClassOfDerivedClassWithCtor"", ""Number"" : 42, ""ExtraProperty"" : ""extra"" }", + ExpectedRoundtripValue: new DerivedClassWithConstructor_TypeDiscriminator.DerivedClass(42, "extra")); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_NoTypeDiscriminator { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClass { Number = 42 }); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_TypeDiscriminator(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedClassWithCustomConverter_TypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + } + + public record TestData( + PolymorphicClass Value, + string? ExpectedJson = null, + Type? ExpectedSerializationException = null, + PolymorphicClass? ExpectedRoundtripValue = null, + Type? ExpectedDeserializationException = null); + } + + [Fact] + public async Task PolymorphicClass_NoTypeDiscriminators_Deserialization_IgnoresTypeMetadata() + { + string json = @"{""$type"" : ""derivedClass""}"; + PolymorphicClass_NoTypeDiscriminators result = await Serializer.DeserializeWrapper(json); + Assert.IsType(result); + Assert.True(result.ExtensionData?.ContainsKey("$type") == true); + } + + [JsonDerivedType(typeof(DerivedClass1))] + [JsonDerivedType(typeof(DerivedClass2))] + public class PolymorphicClass_NoTypeDiscriminators + { + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } + + public class DerivedClass1 : PolymorphicClass_NoTypeDiscriminators { } + public class DerivedClass2 : PolymorphicClass_NoTypeDiscriminators { } + } + + [Fact] + public async Task PolymorphicClass_WithDerivedPolymorphicClass_Serialization_ShouldUseBaseTypeContract() + { + string expectedJson = @"{""$type"":""derivedClass""}"; + PolymorphicClass_WithDerivedPolymorphicClass value = new PolymorphicClass_WithDerivedPolymorphicClass.DerivedClass(); + await TestMultiContextSerialization(value, expectedJson); + } + + [Fact] + public async Task PolymorphicClass_WithDerivedPolymorphicClass_Deserialization_ShouldUseBaseTypeContract() + { + string json = @"{""$type"":""derivedClass""}"; + + var expectedValueUsingBaseContract = new PolymorphicClass_WithDerivedPolymorphicClass.DerivedClass(); + await TestMultiContextDeserialization( + json, + expectedValueUsingBaseContract, + equalityComparer: PolymorphicEqualityComparer.Instance); + + var expectedValueUsingDerivedContract = new PolymorphicClass_WithDerivedPolymorphicClass.DerivedClass.DerivedClass2(); + await TestMultiContextDeserialization( + json, + expectedValueUsingDerivedContract, + equalityComparer: PolymorphicEqualityComparer.Instance); + } + + [JsonDerivedType(typeof(DerivedClass), "derivedClass")] + [JsonDerivedType(typeof(DerivedClass.DerivedClass2), "derivedClass2")] + public class PolymorphicClass_WithDerivedPolymorphicClass + { + // Derived class with conflicting configuration + [JsonDerivedType(typeof(DerivedClass), "baseClass")] + [JsonDerivedType(typeof(DerivedClass.DerivedClass2), "derivedClass")] + public class DerivedClass : PolymorphicClass_WithDerivedPolymorphicClass + { + public class DerivedClass2 : DerivedClass + { + } + } + } + + [Theory] + [ActiveIssue("Need to refactor root-level polymorphic JsonTypeInfo handling.")] + [MemberData(nameof(PolymorphicClass_WithBaseTypeDiscriminator.GetTestData), MemberType = typeof(PolymorphicClass_WithBaseTypeDiscriminator))] + public async Task PolymorphicClass_BoxedSerialization_DoesNotUseTypeDiscriminators(PolymorphicClass_WithBaseTypeDiscriminator value, string expectedJson) + { + await TestMultiContextSerialization(value, expectedJson); + } + + [JsonDerivedType(typeof(PolymorphicClass_WithBaseTypeDiscriminator), "baseType")] + [JsonDerivedType(typeof(DerivedClass), "derivedType")] + public class PolymorphicClass_WithBaseTypeDiscriminator + { + public int Number { get; set; } + + public class DerivedClass : PolymorphicClass_WithBaseTypeDiscriminator + { + public string String { get; set; } + } + + public static IEnumerable GetTestData() + { + yield return WrapArgs(new PolymorphicClass_WithBaseTypeDiscriminator { Number = 42 }, @"{""Number"" : 42 }"); + yield return WrapArgs(new DerivedClass { Number = 42, String = "str" }, @"{""Number"" : 42, ""String"" : str }"); + + static object[] WrapArgs(PolymorphicClass_WithBaseTypeDiscriminator value, string expectedJson) + => new object[] { value, expectedJson }; + } + } + #endregion + + #region Polymorphic Class with Constructor + + [Theory] + [MemberData(nameof(Get_PolymorphicClassWithConstructor_TestData_Serialization))] + public Task PolymorphicClassWithConstructor_TestData_Serialization(PolymorphicClassWithConstructor.TestData testData) + => TestMultiContextSerialization(testData.Value, testData.ExpectedJson); + + public static IEnumerable Get_PolymorphicClassWithConstructor_TestData_Serialization() + => PolymorphicClassWithConstructor.GetSerializeTestData().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicClassWithConstructor_TestData_Deserialization))] + public Task PolymorphicClassWithConstructor_TestData_Deserialization(PolymorphicClassWithConstructor.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + equalityComparer: PolymorphicEqualityComparer.Instance); + + public static IEnumerable Get_PolymorphicClassWithConstructor_TestData_Deserialization() + => PolymorphicClassWithConstructor.GetSerializeTestData() + .Where(entry => entry.ExpectedJson != null) + .Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicClassWithConstructor_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicClassWithConstructor Value, string ExpectedJson)> inputs = + PolymorphicClassWithConstructor.GetSerializeTestData().Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs); + } + + [Fact] + public async Task PolymorphicClassWithConstructor_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicClassWithConstructor ExpectedRoundtripValue)> inputs = + PolymorphicClassWithConstructor.GetSerializeTestData().Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization(inputs, equalityComparer: PolymorphicEqualityComparer.Instance); + } + + [JsonPolymorphic] + [JsonDerivedType(typeof(DerivedClass), "derivedClass")] + [JsonDerivedType(typeof(DerivedClassWithConstructor), "derivedClassWithCtor")] + [JsonDerivedType(typeof(DerivedCollection), "derivedCollection")] + [JsonDerivedType(typeof(DerivedDictionary), "derivedDictionary")] + public class PolymorphicClassWithConstructor + { + [JsonConstructor] + public PolymorphicClassWithConstructor(int number) => Number = number; + + public int Number { get; } + + public class DerivedClass : PolymorphicClassWithConstructor + { + public DerivedClass() : base(0) { } + public string String { get; set; } + } + + public class DerivedClassWithConstructor : PolymorphicClassWithConstructor + { + [JsonConstructor] + public DerivedClassWithConstructor(int number, bool boolean) + : base(number) + { + Boolean = boolean; + } + + public bool Boolean { get; } + } + + public class DerivedCollection : PolymorphicClassWithConstructor, ICollection + { + private List _list = new(); + + public DerivedCollection() : base(0) + { + } + + bool ICollection.IsReadOnly => false; + public void Add(int item) => _list.Add(item); + IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); + int ICollection.Count => _list.Count; + void ICollection.Clear() => throw new NotImplementedException(); + bool ICollection.Contains(int item) => throw new NotImplementedException(); + void ICollection.CopyTo(int[] array, int arrayIndex) => throw new NotImplementedException(); + bool ICollection.Remove(int item) => throw new NotImplementedException(); + } + + public class DerivedDictionary : PolymorphicClassWithConstructor, IDictionary + { + private Dictionary _dict = new(); + + public DerivedDictionary() : base(0) + { + } + + public int this[string key] { get => _dict[key]; set => _dict[key] = value; } + bool ICollection>.IsReadOnly => false; + IEnumerator> IEnumerable>.GetEnumerator() => _dict.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _dict.GetEnumerator(); + + + ICollection IDictionary.Keys => throw new NotImplementedException(); + ICollection IDictionary.Values => throw new NotImplementedException(); + int ICollection>.Count => throw new NotImplementedException(); + void IDictionary.Add(string key, int value) => throw new NotImplementedException(); + void ICollection>.Add(KeyValuePair item) => throw new NotImplementedException(); + void ICollection>.Clear() => throw new NotImplementedException(); + bool ICollection>.Contains(KeyValuePair item) => throw new NotImplementedException(); + bool IDictionary.ContainsKey(string key) => throw new NotImplementedException(); + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => throw new NotImplementedException(); + bool IDictionary.Remove(string key) => throw new NotImplementedException(); + bool ICollection>.Remove(KeyValuePair item) => throw new NotImplementedException(); + bool IDictionary.TryGetValue(string key, out int value) => throw new NotImplementedException(); + } + + public static IEnumerable GetSerializeTestData() + { + yield return new TestData( + Value: new PolymorphicClassWithConstructor(42), + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedRoundtripValue: new PolymorphicClassWithConstructor(42)); + + yield return new TestData( + Value: new DerivedClass { String = "str" }, + ExpectedJson: @"{ ""$type"" : ""derivedClass"", ""Number"" : 0, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedClass { String = "str" }); + + yield return new TestData( + Value: new DerivedClassWithConstructor(42, true), + ExpectedJson: @"{ ""$type"" : ""derivedClassWithCtor"", ""Number"" : 42, ""Boolean"" : true }", + ExpectedRoundtripValue: new DerivedClassWithConstructor(42, true)); + + yield return new TestData( + Value: new DerivedCollection { 1, 2, 3 }, + ExpectedJson: @"{ ""$type"" : ""derivedCollection"", ""$values"" : [1,2,3]}", + ExpectedRoundtripValue: new DerivedCollection { 1, 2, 3 }); + + yield return new TestData( + Value: new DerivedDictionary { ["key1"] = 42, ["key2"] = -1 }, + ExpectedJson: @"{ ""$type"" : ""derivedDictionary"", ""key1"" : 42, ""key2"" : -1 }", + ExpectedRoundtripValue: new DerivedDictionary { ["key1"] = 42, ["key2"] = -1 }); + } + + public record TestData(PolymorphicClassWithConstructor Value, string ExpectedJson, PolymorphicClassWithConstructor ExpectedRoundtripValue); + } + + #endregion + + #region Polymorphic Interface + [Theory] + [MemberData(nameof(Get_PolymorphicInterface_TestData_Serialization))] + public Task PolymorphicInterface_TestData_Serialization(PolymorphicInterface.TestData testData) + => TestMultiContextSerialization(testData.Value, testData.ExpectedJson, testData.ExpectedSerializationException); + + public static IEnumerable Get_PolymorphicInterface_TestData_Serialization() + => PolymorphicInterface.Helpers.GetSerializeTestData().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicInterface_TestData_Deserialization))] + public Task PolymorphicInterface_TestData_Deserialization(PolymorphicInterface.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + testData.ExpectedDeserializationException, + equalityComparer: PolymorphicEqualityComparer.Instance); + + public static IEnumerable Get_PolymorphicInterface_TestData_Deserialization() + => PolymorphicInterface.Helpers.GetSerializeTestData() + .Where(entry => entry.ExpectedJson != null) + .Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicInterface_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicInterface Value, string ExpectedJson)> inputs = + PolymorphicInterface.Helpers.GetSerializeTestData() + .Where(entry => entry.ExpectedSerializationException is null) + .Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs); + } + + [Fact] + public async Task PolymorphicInterface_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicInterface ExpectedRoundtripValue)> inputs = + PolymorphicInterface.Helpers.GetSerializeTestData() + .Where(entry => entry.ExpectedRoundtripValue is not null) + .Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization(inputs, equalityComparer: PolymorphicEqualityComparer.Instance); + } + + [Theory] + [InlineData("$.$type", @"{ ""$type"" : ""derivedClass"", ""$type"" : ""derivedClass"", ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : ""derivedClass"", ""Number"" : 42, ""$type"" : ""derivedClass""}")] + [InlineData("$.$id", @"{ ""$type"" : ""derivedClass"", ""Number"" : 42, ""$id"" : ""referenceId""}")] + [InlineData("$.$values", @"{ ""$type"" : ""derivedClass"", ""Number"" : 42, ""$values"" : [] }")] + [InlineData("$", @"{ ""$type"" : ""invalidDiscriminator"", ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : 0, ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : false, ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : {}, ""Number"" : 42 }")] + [InlineData("$.$type", @"{ ""$type"" : [], ""Number"" : 42 }")] + [InlineData("$.$id", @"{ ""$id"" : ""1"", ""Number"" : 42 }")] + [InlineData("$.$ref", @"{ ""$ref"" : ""1"" }")] + public async Task PolymorphicInterface_InvalidTypeDiscriminatorMetadata_ShouldThrowJsonException(string expectedJsonPath, string json) + { + JsonException exception = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, PolymorphicClass.CustomConfigWithBaseTypeFallback)); + Assert.Equal(expectedJsonPath, exception.Path); + } + + // -- + + [Theory] + [MemberData(nameof(Get_PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestData_Serialization))] + public Task PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestData_Serialization(PolymorphicInterface.TestData testData) + => TestMultiContextSerialization( + testData.Value, + testData.ExpectedJson, + testData.ExpectedSerializationException, + options: PolymorphicInterface.Helpers.CustomConfigWithNearestAncestorFallback); + + public static IEnumerable Get_PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestData_Serialization() + => PolymorphicInterface.Helpers.GetSerializeTestData_CustomConfigWithNearestAncestorFallback().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestData_Deserialization))] + public Task PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestData_Deserialization(PolymorphicInterface.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + testData.ExpectedDeserializationException, + options: PolymorphicInterface.Helpers.CustomConfigWithNearestAncestorFallback, + equalityComparer: PolymorphicEqualityComparer.Instance); + + public static IEnumerable Get_PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestData_Deserialization() + => PolymorphicInterface.Helpers.GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + .Where(entry => entry.ExpectedJson != null) + .Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicInterface Value, string ExpectedJson)> inputs = + PolymorphicInterface.Helpers.GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + .Where(entry => entry.ExpectedSerializationException is null) + .Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs, options: PolymorphicInterface.Helpers.CustomConfigWithNearestAncestorFallback); + } + + [Fact] + public async Task PolymorphicInterface_CustomConfigWithNearestAncestorFallback_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicInterface ExpectedRoundtripValue)> inputs = + PolymorphicInterface.Helpers.GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + .Where(entry => entry.ExpectedRoundtripValue is not null) + .Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization( + inputs, + options: PolymorphicInterface.Helpers.CustomConfigWithNearestAncestorFallback, + equalityComparer: PolymorphicEqualityComparer.Instance); + } + + // -- + + [Theory] + [MemberData(nameof(Get_PolymorphicInterface_DiamondInducingConfigurations_ShouldThrowNotSupportedException))] + public async Task PolymorphicInterface_DiamondInducingConfigurations_ShouldThrowNotSupportedException(PolymorphicInterface value, JsonPolymorphicTypeConfiguration configuration) + { + var options = new JsonSerializerOptions { PolymorphicTypeConfigurations = { configuration } }; + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, options)); + } + + public static IEnumerable Get_PolymorphicInterface_DiamondInducingConfigurations_ShouldThrowNotSupportedException() + => PolymorphicInterface.Helpers.GetDiamondInducingConfigurations().Select(entry => new object[] { entry.diamondValue, entry.configuration }); + + + [JsonDerivedType(typeof(DerivedClass_NoTypeDiscriminator))] + [JsonDerivedType(typeof(DerivedClass_TypeDiscriminator), "derivedClass")] + [JsonDerivedType(typeof(DerivedStruct_NoTypeDiscriminator))] + [JsonDerivedType(typeof(DerivedStruct_TypeDiscriminator), "derivedStruct")] + [JsonDerivedType(typeof(DerivedInterface1.ImplementingClass), "implementingClassOfDerivedInterface")] + public interface PolymorphicInterface + { + public int Number { get; set; } + + public class DerivedClass_NoTypeDiscriminator : PolymorphicInterface + { + public int Number { get; set; } + public string String { get; set; } + + public class DerivedClass : DerivedClass_NoTypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public class DerivedClass_TypeDiscriminator : PolymorphicInterface + { + public int Number { get; set; } + public string String { get; set; } + + public class DerivedClass : DerivedClass_TypeDiscriminator + { + public string ExtraProperty { get; set; } + } + } + + public struct DerivedStruct_NoTypeDiscriminator : PolymorphicInterface + { + public int Number { get; set; } + public string String { get; set; } + } + + public struct DerivedStruct_TypeDiscriminator : PolymorphicInterface + { + public int Number { get; set; } + public string String { get; set; } + } + + public interface DerivedInterface1 : PolymorphicInterface + { + public string String { get; set; } + + public class ImplementingClass : DerivedInterface1 + { + public int Number { get; set; } + public string String { get; set; } + } + } + + public interface DerivedInterface2 : PolymorphicInterface + { + public bool Boolean { get; set; } + } + + public class DiamondKind1 : DerivedInterface1, DerivedInterface2 + { + public int Number { get; set; } + public string String { get; set; } + public bool Boolean { get; set; } + } + + public class DiamondKind2 : DerivedClass_TypeDiscriminator, DerivedInterface1 + { + public bool Boolean { get; set; } + } + + public static class Helpers + { + public static IEnumerable GetSerializeTestData() + { + yield return new TestData( + Value: new DerivedClass_NoTypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"" }", + ExpectedDeserializationException: typeof(NotSupportedException)); + + yield return new TestData( + new DerivedClass_NoTypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedClass_TypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""$type"" : ""derivedClass"", ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedClass_TypeDiscriminator { Number = 42, String = "str" }); + + yield return new TestData( + new DerivedClass_TypeDiscriminator.DerivedClass(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedStruct_NoTypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"" }", + ExpectedDeserializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedStruct_TypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""$type"" : ""derivedStruct"", ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedStruct_TypeDiscriminator { Number = 42, String = "str" }); + } + + public static JsonSerializerOptions CustomConfigWithNearestAncestorFallback { get; } = + new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration + { + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor + } + .WithDerivedType("derivedClass") + .WithDerivedType() + .WithDerivedType() + .WithDerivedType() + } + }; + + public static IEnumerable GetSerializeTestData_CustomConfigWithNearestAncestorFallback() + { + yield return new TestData( + Value: new DerivedClass_NoTypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedDeserializationException: typeof(NotSupportedException)); + + yield return new TestData( + new DerivedClass_NoTypeDiscriminator.DerivedClass { Number = 42 }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedDeserializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedClass_TypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""$type"" : ""derivedClass"", ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedClass_TypeDiscriminator { Number = 42, String = "str" }); + + yield return new TestData( + new DerivedClass_TypeDiscriminator.DerivedClass { Number = 42, String = "str", ExtraProperty = "extra" }, + ExpectedJson: @"{ ""$type"" : ""derivedClass"", ""Number"" : 42, ""String"" : ""str"" }", + ExpectedRoundtripValue: new DerivedClass_TypeDiscriminator { Number = 42, String = "str" }); + + yield return new TestData( + Value: new DerivedStruct_NoTypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42, ""String"" : ""str"" }", + ExpectedDeserializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DerivedStruct_TypeDiscriminator { Number = 42, String = "str" }, + ExpectedJson: @"{ ""Number"" : 42 }", + ExpectedDeserializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DiamondKind1(), + ExpectedSerializationException: typeof(NotSupportedException)); + + yield return new TestData( + Value: new DiamondKind2(), + ExpectedSerializationException: typeof(NotSupportedException)); + + } + + public static IEnumerable<(PolymorphicInterface diamondValue, JsonPolymorphicTypeConfiguration configuration)> GetDiamondInducingConfigurations() + { + yield return ( + new DiamondKind1(), + new JsonPolymorphicTypeConfiguration { UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor } + .WithDerivedType() + .WithDerivedType()); + + yield return ( + new DiamondKind2(), + new JsonPolymorphicTypeConfiguration { UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor } + .WithDerivedType() + .WithDerivedType()); + } + } + + public record TestData( + PolymorphicInterface Value = null, + string? ExpectedJson = null, + PolymorphicInterface? ExpectedRoundtripValue = null, + Type? ExpectedSerializationException = null, + Type? ExpectedDeserializationException = null); + } + #endregion + + #region Polymorphic List + + [Theory] + [MemberData(nameof(Get_PolymorphicList_TestData_Serialization))] + public Task PolymorphicList_TestData_Serialization(PolymorphicList.TestData testData) + => TestMultiContextSerialization(testData.Value, testData.ExpectedJson); + + public static IEnumerable Get_PolymorphicList_TestData_Serialization() + => PolymorphicList.GetSerializeTestData().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicList_TestData_Serialization))] + public Task PolymorphicList_TestData_Deserialization(PolymorphicList.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + equalityComparer: PolymorphicEqualityComparer.Instance); + + public static IEnumerable Get_PolymorphicList_TestData_Deserialization() + => PolymorphicList.GetSerializeTestData().Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicList_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicList Value, string ExpectedJson)> inputs = + PolymorphicList.GetSerializeTestData().Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs); + } + + [Fact] + public async Task PolymorphicList_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicList ExpectedRoundtripValue)> inputs = + PolymorphicList.GetSerializeTestData().Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization(inputs, equalityComparer: PolymorphicEqualityComparer.Instance); + } + + [Fact] + public async Task PolymorphicList_UnrecognizedTypeDiscriminators_ShouldSucceedDeserialization() + { + string json = @"{ ""$type"" : ""invalidTypeDiscriminator"", ""$values"" : [42,42,42] }"; + PolymorphicList result = await Serializer.DeserializeWrapper(json); + Assert.IsType(result); + Assert.Equal(Enumerable.Repeat(42, 3), result); + } + + [Theory] + [InlineData("$.UnsupportedProperty", @"{ ""$type"" : ""derivedList"", ""UnsupportedProperty"" : 42 }")] + [InlineData("$.UnsupportedProperty", @"{ ""$type"" : ""derivedList"", ""$values"" : [], ""UnsupportedProperty"" : 42 }")] + [InlineData("$.$id", @"{ ""$id"" : 42, ""$values"" : [] }")] + [InlineData("$.$ref", @"{ ""$ref"" : 42 }")] + public async Task PolymorphicList_InvalidTypeDiscriminatorMetadata_ShouldThrowJsonException(string expectedJsonPath, string json) + { + JsonException exception = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Equal(expectedJsonPath, exception.Path); + } + + [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor, IgnoreUnrecognizedTypeDiscriminators = true)] + [JsonDerivedType(typeof(PolymorphicList), "baseList")] + [JsonDerivedType(typeof(DerivedList1), "derivedList")] + public class PolymorphicList : List + { + public class DerivedList1 : PolymorphicList + { + } + + public class DerivedList2 : PolymorphicList + { + } + + public static IEnumerable GetSerializeTestData() + { + yield return new TestData( + Value: new PolymorphicList { 42 }, + ExpectedJson: @"{ ""$type"" : ""baseList"", ""$values"" : [42]}", + ExpectedRoundtripValue: new PolymorphicList { 42 }); + + yield return new TestData( + Value: new DerivedList1 { 42 }, + ExpectedJson: @"{ ""$type"" : ""derivedList"", ""$values"" : [42]}", + ExpectedRoundtripValue: new DerivedList1 { 42 }); + + yield return new TestData( + Value: new DerivedList2 { 42 }, + ExpectedJson: @"{ ""$type"" : ""baseList"", ""$values"" : [42]}", + ExpectedRoundtripValue: new PolymorphicList { 42 }); + } + + public record TestData(PolymorphicList Value, string ExpectedJson, PolymorphicList ExpectedRoundtripValue); + } + + [Fact] + public async Task PolymorphicCollectionInterface_Serialization() + { + var source = new int[] { 1, 2, 3 }; + var values = new IEnumerable[] + { + source, + new List(source), + new Queue(source), + new HashSet(source) + }; + + string expectedJson = + @"[ [1,2,3], + { ""$type"":""list"" , ""$values"":[1,2,3] }, + { ""$type"":""queue"", ""$values"":[1,2,3] }, + { ""$type"":""set"" , ""$values"":[1,2,3] }]"; + + string actualJson = await Serializer.SerializeWrapper(values, s_optionsWithPolymorphicCollectionInterface); + + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task PolymorphicCollectionInterface_Deserialization() + { + var source = new int[] { 1, 2, 3 }; + var expectedValues = new IEnumerable[] + { + new List(source), + new List(source), + new Queue(source), + new HashSet(source) + }; + + string json = + @"[ [1,2,3], + { ""$type"":""list"" , ""$values"":[1,2,3] }, + { ""$type"":""queue"", ""$values"":[1,2,3] }, + { ""$type"":""set"" , ""$values"":[1,2,3] }]"; + + var actualValues = await Serializer.DeserializeWrapper[]>(json, s_optionsWithPolymorphicCollectionInterface); + Assert.Equal(expectedValues.Length, actualValues.Length); + for (int i = 0; i < expectedValues.Length; i++) + { + Assert.Equal(expectedValues[i], actualValues[i]); + Assert.IsType(expectedValues[i].GetType(), actualValues[i]); + } + } + + private readonly static JsonSerializerOptions s_optionsWithPolymorphicCollectionInterface = new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration> + { + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor + } + .WithDerivedType>("list") + .WithDerivedType>("queue") + .WithDerivedType>("set") + } + }; + #endregion + + #region Polymorphic Dictionary + [Theory] + [MemberData(nameof(Get_PolymorphicDictionary_TestData_Serialization))] + public Task PolymorphicDictionary_TestData_Serialization(PolymorphicDictionary.TestData testData) + => TestMultiContextSerialization(testData.Value, testData.ExpectedJson); + + public static IEnumerable Get_PolymorphicDictionary_TestData_Serialization() + => PolymorphicDictionary.GetSerializeTestData().Select(entry => new object[] { entry }); + + [Theory] + [MemberData(nameof(Get_PolymorphicDictionary_TestData_Serialization))] + public Task PolymorphicDictionary_TestData_Deserialization(PolymorphicDictionary.TestData testData) + => TestMultiContextDeserialization( + testData.ExpectedJson, + testData.ExpectedRoundtripValue, + equalityComparer: PolymorphicEqualityComparer.Instance); + + public static IEnumerable Get_PolymorphicDictionary_TestData_Deserialization() + => PolymorphicDictionary.GetSerializeTestData().Select(entry => new object[] { entry }); + + [Fact] + public async Task PolymorphicDictionary_TestDataArray_Serialization() + { + IEnumerable<(PolymorphicDictionary Value, string ExpectedJson)> inputs = + PolymorphicDictionary.GetSerializeTestData().Select(entry => (entry.Value, entry.ExpectedJson)); + + await TestMultiContextSerialization(inputs); + } + + [Fact] + public async Task PolymorphicDictionary_TestDataArray_Deserialization() + { + IEnumerable<(string ExpectedJson, PolymorphicDictionary ExpectedRoundtripValue)> inputs = + PolymorphicDictionary.GetSerializeTestData().Select(entry => (entry.ExpectedJson, entry.ExpectedRoundtripValue)); + + await TestMultiContextDeserialization(inputs, equalityComparer: PolymorphicEqualityComparer.Instance); + } + + [Fact] + public async Task PolymorphicDictionary_UnrecognizedTypeDiscriminators_ShouldSucceedDeserialization() + { + string json = @"{ ""$type"" : ""invalidTypeDiscriminator"", ""key"" : 42 }"; + PolymorphicDictionary result = await Serializer.DeserializeWrapper(json); + Assert.IsType(result); + Assert.Equal(new PolymorphicDictionary { ["key"] = 42 }, result); + } + + [Theory] + [InlineData("$.$ref", @"{ ""$type"" : ""derivedList"", ""UserProperty"" : 42, ""$ref"" : ""42"" }")] + [InlineData("$.$type", @"{ ""$type"" : ""derivedList"", ""UserProperty"" : 42, ""$type"" : ""derivedDictionary"" }")] + [InlineData("$.$type", @"{ ""UserProperty"" : 42, ""$type"" : ""derivedDictionary"" }")] + [InlineData("$.$values", @"{ ""$type"" : ""derivedDictionary"", ""$values"" : [] }")] + [InlineData("$.$id", @"{ ""$id"" : 42, ""UserProperty"" : 42 }")] + [InlineData("$.$ref", @"{ ""$ref"" : 42 }")] + public async Task PolymorphicDictionary_InvalidTypeDiscriminatorMetadata_ShouldThrowJsonException(string expectedJsonPath, string json) + { + JsonException exception = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + Assert.Equal(expectedJsonPath, exception.Path); + } + + [JsonPolymorphic(UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor, IgnoreUnrecognizedTypeDiscriminators = true)] + [JsonDerivedType(typeof(PolymorphicDictionary), "baseDictionary")] + [JsonDerivedType(typeof(DerivedDictionary1), "derivedDictionary")] + public class PolymorphicDictionary : Dictionary + { + public class DerivedDictionary1 : PolymorphicDictionary + { + } + + public class DerivedDictionary2 : PolymorphicDictionary + { + } + + public static IEnumerable GetSerializeTestData() + { + yield return new TestData( + Value: new PolymorphicDictionary { ["key1"] = 42 , ["key2"] = -1 }, + ExpectedJson: @"{ ""$type"" : ""baseDictionary"", ""key1"" : 42, ""key2"" : -1 }", + ExpectedRoundtripValue: new PolymorphicDictionary { ["key1"] = 42, ["key2"] = -1 }); + + yield return new TestData( + Value: new DerivedDictionary1 { ["key1"] = 42, ["key2"] = -1 }, + ExpectedJson: @"{ ""$type"" : ""derivedDictionary"", ""key1"" : 42, ""key2"" : -1 }", + ExpectedRoundtripValue: new DerivedDictionary1 { ["key1"] = 42, ["key2"] = -1 }); + + yield return new TestData( + Value: new DerivedDictionary2 { ["key1"] = 42, ["key2"] = -1 }, + ExpectedJson: @"{ ""$type"" : ""baseDictionary"", ""key1"" : 42, ""key2"" : -1 }", + ExpectedRoundtripValue: new PolymorphicDictionary { ["key1"] = 42, ["key2"] = -1 }); + } + + public record TestData(PolymorphicDictionary Value, string ExpectedJson, PolymorphicDictionary ExpectedRoundtripValue); + } + + [Fact] + public async Task PolymorphicDictionaryInterface_Serialization() + { + var values = new IEnumerable>[] + { + new List> { new KeyValuePair(0, 0) }, + new Dictionary { [42] = false }, + new SortedDictionary { [0] = 1, [1] = 42 }, + ImmutableDictionary.Create() + }; + + string expectedJson = + @"[ [ { ""Key"":0, ""Value"":0 } ], + { ""$type"" : ""dictionary"", ""42"" : false }, + { ""$type"" : ""sortedDictionary"", ""0"" : 1, ""1"" : 42 }, + { ""$type"" : ""readOnlyDictionary"" } ]"; + + string actualJson = await Serializer.SerializeWrapper(values, s_optionsWithPolymorphicDictionaryInterface); + + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task PolymorphicDictionaryInterface_Deserialization() + { + string json = + @"[ [ { ""Key"":0, ""Value"":0 } ], + { ""$type"" : ""dictionary"", ""42"" : false }, + { ""$type"" : ""sortedDictionary"", ""0"" : 1, ""1"" : 42 }, + { ""$type"" : ""readOnlyDictionary"" } ]"; + + var expectedValues = new IEnumerable>[] + { + new List> { new KeyValuePair(0, 0) }, + new Dictionary { [42] = false }, + new SortedDictionary { [0] = 1, [1] = 42 }, + new Dictionary() + }; + + var actualValues = await Serializer.DeserializeWrapper>[]>(json, s_optionsWithPolymorphicDictionaryInterface); + + Assert.Equal(expectedValues.Length, actualValues.Length); + for (int i = 0; i < expectedValues.Length; i++) + { + Assert.Equal(expectedValues[i].Select(x => x.Key), actualValues[i].Select(x => x.Key)); + Assert.Equal(expectedValues[i].Select(x => x.Value.ToString()), actualValues[i].Select(x => x.Value.ToString())); + Assert.IsType(expectedValues[i].GetType(), actualValues[i]); + } + } + + private readonly static JsonSerializerOptions s_optionsWithPolymorphicDictionaryInterface = new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration>> + { + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor + } + .WithDerivedType>("dictionary") + .WithDerivedType>("sortedDictionary") + .WithDerivedType>("readOnlyDictionary") + } + }; + #endregion + + #region Polymorphic Record Types + [Theory] + [InlineData(0, @"{""$type"":""zero""}")] + [InlineData(1, @"{""$type"":""succ"", ""value"":{""$type"":""zero""}}")] + [InlineData(3, @"{""$type"":""succ"", ""value"":{""$type"":""succ"",""value"":{""$type"":""succ"",""value"":{""$type"":""zero""}}}}")] + public async Task Peano_Serialization(int size, string expectedJson) + { + Peano peano = Peano.FromInteger(size); + await TestMultiContextSerialization(peano, expectedJson); + } + + [Theory] + [InlineData(0, @"{""$type"":""zero""}")] + [InlineData(1, @"{""$type"":""succ"", ""value"":{""$type"":""zero""}}")] + [InlineData(3, @"{""$type"":""succ"", ""value"":{""$type"":""succ"",""value"":{""$type"":""succ"",""value"":{""$type"":""zero""}}}}")] + public async Task Peano_Deserialization(int expectedSize, string json) + { + Peano expected = Peano.FromInteger(expectedSize); + await TestMultiContextDeserialization(json, expected); + } + + // A Peano representation for natural numbers + [JsonDerivedType(typeof(Zero), "zero")] + [JsonDerivedType(typeof(Succ), "succ")] + public abstract record Peano + { + public static Peano FromInteger(int value) => value == 0 ? new Zero() : new Succ(FromInteger(value - 1)); + public record Zero : Peano; + public record Succ(Peano value) : Peano; + } + + [Theory] + [MemberData(nameof(BinaryTree.GetTestData), MemberType = typeof(BinaryTree))] + public async Task BinaryTree_TestData_Serialization(BinaryTree tree, string expectedJson) + { + string actualJson = await Serializer.SerializeWrapper(tree); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(BinaryTree.GetTestData), MemberType = typeof(BinaryTree))] + public async Task BinaryTree_TestData_Deserialization(BinaryTree expected, string json) + { + BinaryTree actual = await Serializer.DeserializeWrapper(json); + Assert.Equal(expected, actual); + } + + [JsonDerivedType(typeof(Leaf), "leaf")] + [JsonDerivedType(typeof(Node), "node")] + public abstract record BinaryTree + { + public record Leaf : BinaryTree; + public record Node(int value, BinaryTree left, BinaryTree right) : BinaryTree; + + public static IEnumerable GetTestData() + { + yield return WrapArgs(new Leaf(), @"{""$type"":""leaf""}"); + yield return WrapArgs( + new Node(-1, + new Leaf(), + new Leaf()), + @"{""$type"":""node"",""value"":-1,""left"":{""$type"":""leaf""},""right"":{""$type"":""leaf""}}"); + + yield return WrapArgs( + new Node(12, + new Leaf(), + new Node(24, + new Leaf(), + new Leaf())), + @"{""$type"":""node"", ""value"":12, + ""left"":{""$type"":""leaf""}, + ""right"":{""$type"":""node"", ""value"":24, + ""left"":{""$type"":""leaf""}, + ""right"":{""$type"":""leaf""}}}"); + + static object[] WrapArgs(BinaryTree value, string expectedJson) => new object[] { value, expectedJson }; + } + } + + #endregion + + #region Polymorphism/Reference Preservation + + [Theory] + [MemberData(nameof(Get_ReferencePreservation_TestData_Boxed))] + public async Task ReferencePreservation_SingleValue_Serialization(PolymorphicClass value, Func jsonTemplate) + { + string expectedJson = jsonTemplate("1"); // root values have reference id "1" + string actualJson = await Serializer.SerializeWrapper(value, s_jsonSerializerOptionsPreserveRefs); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(Get_ReferencePreservation_TestData_Boxed))] + public async Task ReferencePreservation_SingleValue_Deserialization(PolymorphicClass expectedValue, Func jsonTemplate) + { + string json = jsonTemplate("1"); // root values have reference id "1" + PolymorphicClass actualValue = await Serializer.DeserializeWrapper(json, s_jsonSerializerOptionsPreserveRefs); + Assert.Equal(expectedValue, actualValue, PolymorphicEqualityComparer.Instance); + } + + [Theory] + [MemberData(nameof(Get_ReferencePreservation_TestData_Boxed))] + public async Task ReferencePreservation_RepeatingValue_Serialization(PolymorphicClass value, Func jsonTemplate) + { + List input = new() { value, value }; + string expectedJson = + $@"{{""$id"":""1"", + ""$values"":[ + {jsonTemplate("2")}, + {{""$ref"":""2""}} ] + }}"; + + string actualJson = await Serializer.SerializeWrapper(input, s_jsonSerializerOptionsPreserveRefs); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(Get_ReferencePreservation_TestData_Boxed))] + public async Task ReferencePreservation_RepeatingValue_Deserialization(PolymorphicClass expectedValue, Func jsonTemplate) + { + string json = + $@"{{""$id"":""1"", + ""$values"":[ + {jsonTemplate("2")}, + {{""$ref"":""2""}} ] + }}"; + + var result = await Serializer.DeserializeWrapper>(json, s_jsonSerializerOptionsPreserveRefs); + + Assert.Equal(2, result.Count); + Assert.Equal(expectedValue, result[0], PolymorphicEqualityComparer.Instance); + Assert.Same(result[0], result[1]); + } + + [Fact] + public async Task ReferencePreservation_MultipleRepeatingValues_Serialization() + { + (PolymorphicClass Value, Func JsonTemplate)[] data = Get_ReferencePreservation_TestData().ToArray(); + PolymorphicClass[] values = data.Select(entry => entry.Value).Concat(data.Select(entry => entry.Value)).ToArray(); + + IEnumerable idValues = data.Select((entry, i) => entry.JsonTemplate((i + 1).ToString())); + IEnumerable refValues = Enumerable.Range(1, data.Length).Select(x => $@"{{ ""$ref"" : ""{x}""}}"); + string expectedJson = "[" + string.Join(", ", idValues.Concat(refValues)) + "]"; + + string actualJson = await Serializer.SerializeWrapper(values, s_jsonSerializerOptionsPreserveRefs); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task ReferencePreservation_MultipleRepeatingValues_Deserialization() + { + (PolymorphicClass Value, Func JsonTemplate)[] data = Get_ReferencePreservation_TestData().ToArray(); + PolymorphicClass[] expectedValues = data.Select(entry => entry.Value).Concat(data.Select(entry => entry.Value)).ToArray(); + + IEnumerable idValues = data.Select((entry, i) => entry.JsonTemplate((i + 1).ToString())); + IEnumerable refValues = Enumerable.Range(1, data.Length).Select(x => $@"{{ ""$ref"" : ""{x}""}}"); + string json = "[" + string.Join(", ", idValues.Concat(refValues)) + "]"; + + PolymorphicClass[] result = await Serializer.DeserializeWrapper(json, s_jsonSerializerOptionsPreserveRefs); + Assert.Equal(expectedValues, result, PolymorphicEqualityComparer.Instance); + } + + public static IEnumerable<(PolymorphicClass Value, Func JsonTemplate)> Get_ReferencePreservation_TestData() + { + yield return ( + Value: new PolymorphicClass.DerivedClass1_TypeDiscriminator { Number = 42, String = "str" }, + JsonTemplate: id => $@"{{""$id"":""{id}"",""$type"":""derivedClass1"",""Number"":42,""String"":""str""}}"); + + yield return ( + Value: new PolymorphicClass.DerivedClassWithConstructor_TypeDiscriminator(42), + JsonTemplate: id => $@"{{""$id"":""{id}"",""$type"":""derivedClassWithCtor"",""Number"":42}}"); + + yield return ( + Value: new PolymorphicClass.DerivedCollection_TypeDiscriminator { Number = 42 }, + JsonTemplate: id => $@"{{""$id"":""{id}"",""$type"":""derivedCollection"",""$values"":[42,42,42]}}"); + + yield return ( + Value: new PolymorphicClass.DerivedDictionary_TypeDiscriminator { Number = 42 }, + JsonTemplate: id => $@"{{""$id"":""{id}"",""$type"":""derivedDictionary"",""dictionaryKey"":42}}"); + } + + public static IEnumerable Get_ReferencePreservation_TestData_Boxed() + => Get_ReferencePreservation_TestData().Select(entry => new object[] { entry.Value, entry.JsonTemplate }); + + [Theory] + [MemberData(nameof(PolymorphicClassWithCustomTypeDiscriminator.GetTestData_Boxed), MemberType = typeof(PolymorphicClassWithCustomTypeDiscriminator))] + public async Task ReferencePreservation_CustomTypeDiscriminator_SingleValue_Serialization(PolymorphicClassWithCustomTypeDiscriminator value, Func jsonTemplate) + { + string expectedJson = jsonTemplate("1"); // root values have reference id "1" + string actualJson = await Serializer.SerializeWrapper(value, s_jsonSerializerOptionsPreserveRefs); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(PolymorphicClassWithCustomTypeDiscriminator.GetTestData_Boxed), MemberType = typeof(PolymorphicClassWithCustomTypeDiscriminator))] + public async Task ReferencePreservation_CustomTypeDiscriminator_SingleValue_Deserialization(PolymorphicClassWithCustomTypeDiscriminator expectedValue, Func jsonTemplate) + { + string json = jsonTemplate("1"); // root values have reference id "1" + PolymorphicClassWithCustomTypeDiscriminator actualValue = await Serializer.DeserializeWrapper(json, s_jsonSerializerOptionsPreserveRefs); + Assert.Equal(expectedValue, actualValue, PolymorphicEqualityComparer.Instance); + } + + [Theory] + [MemberData(nameof(PolymorphicClassWithCustomTypeDiscriminator.GetTestData_Boxed), MemberType = typeof(PolymorphicClassWithCustomTypeDiscriminator))] + public async Task ReferencePreservation_CustomTypeDiscriminator_RepeatingValue_Serialization(PolymorphicClassWithCustomTypeDiscriminator value, Func jsonTemplate) + { + List input = new() { value, value }; + string expectedJson = + $@"{{""$id"":""1"", + ""$values"":[ + {jsonTemplate("2")}, + {{""$ref"":""2""}} ] + }}"; + + string actualJson = await Serializer.SerializeWrapper(input, s_jsonSerializerOptionsPreserveRefs); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [MemberData(nameof(PolymorphicClassWithCustomTypeDiscriminator.GetTestData_Boxed), MemberType = typeof(PolymorphicClassWithCustomTypeDiscriminator))] + public async Task ReferencePreservation_CustomTypeDiscriminator_RepeatingValue_Deserialization(PolymorphicClassWithCustomTypeDiscriminator expectedValue, Func jsonTemplate) + { + string json = + $@"{{""$id"":""1"", + ""$values"":[ + {jsonTemplate("2")}, + {{""$ref"":""2""}} ] + }}"; + + var result = await Serializer.DeserializeWrapper>(json, s_jsonSerializerOptionsPreserveRefs); + + Assert.Equal(2, result.Count); + Assert.Equal(expectedValue, result[0], PolymorphicEqualityComparer.Instance); + Assert.Same(result[0], result[1]); + } + + [Fact] + public async Task ReferencePreservation_CustomTypeDiscriminator_MultipleRepeatingValues_Serialization() + { + (PolymorphicClassWithCustomTypeDiscriminator Value, Func JsonTemplate)[] data = PolymorphicClassWithCustomTypeDiscriminator.GetTestData().ToArray(); + PolymorphicClassWithCustomTypeDiscriminator[] values = data.Select(entry => entry.Value).Concat(data.Select(entry => entry.Value)).ToArray(); + + IEnumerable idValues = data.Select((entry, i) => entry.JsonTemplate((i + 1).ToString())); + IEnumerable refValues = Enumerable.Range(1, data.Length).Select(x => $@"{{ ""$ref"" : ""{x}""}}"); + string expectedJson = "[" + string.Join(", ", idValues.Concat(refValues)) + "]"; + + string actualJson = await Serializer.SerializeWrapper(values, s_jsonSerializerOptionsPreserveRefs); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task ReferencePreservation_CustomTypeDiscriminator_MultipleRepeatingValues_Deserialization() + { + (PolymorphicClassWithCustomTypeDiscriminator Value, Func JsonTemplate)[] data = PolymorphicClassWithCustomTypeDiscriminator.GetTestData().ToArray(); + PolymorphicClassWithCustomTypeDiscriminator[] expectedValues = data.Select(entry => entry.Value).Concat(data.Select(entry => entry.Value)).ToArray(); + + IEnumerable idValues = data.Select((entry, i) => entry.JsonTemplate((i + 1).ToString())); + IEnumerable refValues = Enumerable.Range(1, data.Length).Select(x => $@"{{ ""$ref"" : ""{x}""}}"); + string json = "[" + string.Join(", ", idValues.Concat(refValues)) + "]"; + + PolymorphicClassWithCustomTypeDiscriminator[] result = await Serializer.DeserializeWrapper(json, s_jsonSerializerOptionsPreserveRefs); + Assert.Equal(expectedValues, result, PolymorphicEqualityComparer.Instance); + } + + [JsonPolymorphic(CustomTypeDiscriminatorPropertyName = "case")] + [JsonDerivedType(typeof(PolymorphicClassWithCustomTypeDiscriminator), "baseClass")] + [JsonDerivedType(typeof(DerivedClass), "derivedClass")] + [JsonDerivedType(typeof(DerivedCollection), "derivedCollection")] + public class PolymorphicClassWithCustomTypeDiscriminator + { + public int Number { get; set; } + + public class DerivedClass : PolymorphicClassWithCustomTypeDiscriminator + { + public string String { get; set; } + } + + public class DerivedCollection : PolymorphicClassWithCustomTypeDiscriminator, ICollection + { + public bool IsReadOnly => false; + public void Add(int item) => Number = item; + public IEnumerator GetEnumerator() => Enumerable.Repeat(Number, 3).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => throw new NotImplementedException(); + public void Clear() => throw new NotImplementedException(); + public bool Contains(int item) => throw new NotImplementedException(); + public void CopyTo(int[] array, int arrayIndex) => throw new NotImplementedException(); + public bool Remove(int item) => throw new NotImplementedException(); + } + + public static IEnumerable<(PolymorphicClassWithCustomTypeDiscriminator Value, Func JsonTemplate)> GetTestData() + { + yield return ( + Value: new PolymorphicClassWithCustomTypeDiscriminator { Number = 42 }, + JsonTemplate: id => $@"{{""$id"":""{id}"",""case"":""baseClass"",""Number"":42}}"); + + yield return ( + Value: new DerivedClass { Number = 42, String = "str" }, + JsonTemplate: id => $@"{{""case"":""derivedClass"",""$id"":""{id}"",""Number"":42,""String"":""str""}}"); + + yield return ( + Value: new DerivedCollection { 42 }, + JsonTemplate: id => $@"{{""case"":""derivedCollection"",""$id"":""{id}"",""$values"":[42,42,42]}}"); + } + + public static IEnumerable GetTestData_Boxed() + => GetTestData().Select(entry => new object[] { entry.Value, entry.JsonTemplate }); + } + + private readonly static JsonSerializerOptions s_jsonSerializerOptionsPreserveRefs = new JsonSerializerOptions + { + ReferenceHandler = ReferenceHandler.Preserve + }; + #endregion + + #region Attribute Negative Tests + + [Fact] + public async Task PolymorphicClassWithoutDerivedTypeAttribute_ThrowsInvalidOperationException() + { + var value = new PolymorphicClassWithoutDerivedTypeAttribute(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonPolymorphic] + public class PolymorphicClassWithoutDerivedTypeAttribute + { + } + + [Fact] + public async Task PolymorphicClassWithNullDerivedTypeAttribute_ThrowsInvalidOperationException() + { + var value = new PolymorphicClassWithNullDerivedTypeAttribute(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(derivedType: null)] + public class PolymorphicClassWithNullDerivedTypeAttribute + { + } + + [Fact] + public async Task PolymorphicClassWithStructDerivedTypeAttribute_ThrowsInvalidOperationException() + { + var value = new PolymorphicClassWithStructDerivedTypeAttribute(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(Guid))] + public class PolymorphicClassWithStructDerivedTypeAttribute + { + } + + [Fact] + public async Task PolymorphicClassWithObjectDerivedTypeAttribute_ThrowsInvalidOperationException() + { + var value = new PolymorphicClassWithObjectDerivedTypeAttribute(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(object), "object")] + public class PolymorphicClassWithObjectDerivedTypeAttribute + { + } + + [Fact] + public async Task PolymorphicClassWithNonAssignableDerivedTypeAttribute_ThrowsInvalidOperationException() + { + var value = new PolymorphicClassWithNonAssignableDerivedTypeAttribute(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(object))] + public class PolymorphicClassWithNonAssignableDerivedTypeAttribute + { + } + + + [Fact] + public async Task PolymorphicInterfaceWithInterfaceDerivedType_Serialization_ThrowsInvalidOperationException() + { + PolymorphicInterfaceWithInterfaceDerivedType value = new PolymorphicInterfaceWithInterfaceDerivedType.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [Fact] + public async Task PolymorphicInterfaceWithInterfaceDerivedType_Deserialization_ThrowsInvalidOperationException() + { + string json = @"{""$type"":""derivedInterface""}"; + await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + } + + [Fact] + public async Task PolymorphicInterfaceWithInterfaceDerivedType_FallbackToNearestAncestor_Serialization() + { + PolymorphicInterfaceWithInterfaceDerivedType value = new PolymorphicInterfaceWithInterfaceDerivedType.DerivedInterface.ImplementingClass(); + string expectedJson = @"{""$type"":""derivedInterface""}"; + string actualJson = await Serializer.SerializeWrapper(value, PolymorphicInterfaceWithInterfaceDerivedType_OptionsWithFallbackToNearestAncestor); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task PolymorphicInterfaceWithInterfaceDerivedType_FallbackToNearestAncestor_Deserialization_ThrowsNotSupportedException() + { + string json = @"{""$type"":""derivedInterface""}"; + await Assert.ThrowsAsync(() => + Serializer.DeserializeWrapper(json, + PolymorphicInterfaceWithInterfaceDerivedType_OptionsWithFallbackToNearestAncestor)); + } + + [JsonDerivedType(typeof(DerivedInterface), "derivedInterface")] + [JsonDerivedType(typeof(DerivedClass), "derivedClass")] + public interface PolymorphicInterfaceWithInterfaceDerivedType + { + public interface DerivedInterface : PolymorphicInterfaceWithInterfaceDerivedType + { + public class ImplementingClass : DerivedInterface + { + } + } + + public class DerivedClass : PolymorphicInterfaceWithInterfaceDerivedType + { + } + + } + + public static JsonSerializerOptions PolymorphicInterfaceWithInterfaceDerivedType_OptionsWithFallbackToNearestAncestor { get; } = + new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration() + { + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallbackToNearestAncestor + } + .WithDerivedType("derivedInterface") + .WithDerivedType("derivedClass") + } + }; + + [Fact] + public async Task PolymorphicAbstractClassWithAbstractClassDerivedType_ThrowsInvalidOperationException() + { + PolymorphicAbstractClassWithAbstractClassDerivedType value = new PolymorphicAbstractClassWithAbstractClassDerivedType.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(DerivedAbstractClass))] + [JsonDerivedType(typeof(DerivedClass))] + public abstract class PolymorphicAbstractClassWithAbstractClassDerivedType + { + public abstract class DerivedAbstractClass : PolymorphicAbstractClassWithAbstractClassDerivedType + { + } + + public class DerivedClass : PolymorphicAbstractClassWithAbstractClassDerivedType + { + } + } + + [Fact] + public async Task PolymorphicClassWithDuplicateDerivedTypeRegistrations_ThrowsInvalidOperationException() + { + PolymorphicClassWithDuplicateDerivedTypeRegistrations value = new PolymorphicClassWithDuplicateDerivedTypeRegistrations.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(DerivedClass))] + [JsonDerivedType(typeof(DerivedClass), "id")] + public class PolymorphicClassWithDuplicateDerivedTypeRegistrations + { + public class DerivedClass : PolymorphicClassWithDuplicateDerivedTypeRegistrations + { + } + } + + [Fact] + public async Task PolymorphicClasWithDuplicateTypeDiscriminators_ThrowsInvalidOperationException() + { + var value = new PolymorphicClasWithDuplicateTypeDiscriminators(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(A), "duplicateId")] + [JsonDerivedType(typeof(B), "duplicateId")] + public class PolymorphicClasWithDuplicateTypeDiscriminators + { + public class A : PolymorphicClasWithDuplicateTypeDiscriminators { } + public class B : PolymorphicClasWithDuplicateTypeDiscriminators { } + } + + [Fact] + public async Task PolymorphicGenericClass_ThrowsInvalidOperationException() + { + PolymorphicGenericClass value = new PolymorphicGenericClass.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(PolymorphicGenericClass<>.DerivedClass))] + public class PolymorphicGenericClass + { + public class DerivedClass : PolymorphicGenericClass + { + } + } + + [Fact] + public async Task PolymorphicDerivedGenericClass_ThrowsInvalidOperationException() + { + PolymorphicDerivedGenericClass value = new PolymorphicDerivedGenericClass.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [JsonDerivedType(typeof(DerivedClass<>))] + public class PolymorphicDerivedGenericClass + { + public class DerivedClass : PolymorphicDerivedGenericClass + { + } + } + + [Fact] + public async Task PolymorphicClass_CustomConverter_TypeDiscriminator_Serialization_ThrowsNotSupportedException() + { + PolymorphicClass_CustomConverter_TypeDiscriminator value = new PolymorphicClass_CustomConverter_TypeDiscriminator.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value)); + } + + [Fact] + public async Task PolymorphicClass_CustomConverter_TypeDiscriminator_Deserialization_ThrowsNotSupportedException() + { + string json = @"{ ""$type"" : ""derivedClass"" }"; + await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json)); + } + + [JsonConverter(typeof(CustomConverter))] + [JsonDerivedType(typeof(DerivedClass), "derivedClass")] + public class PolymorphicClass_CustomConverter_TypeDiscriminator + { + public class DerivedClass : PolymorphicClass_CustomConverter_TypeDiscriminator + { + } + + public class CustomConverter : JsonConverter + { + public override PolymorphicClass_CustomConverter_TypeDiscriminator? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + reader.TrySkip(); + return null; + } + + public override void Write(Utf8JsonWriter writer, PolymorphicClass_CustomConverter_TypeDiscriminator value, JsonSerializerOptions options) + => writer.WriteNullValue(); + } + } + + [Fact] + public async Task PolymorphicClass_CustomConverter_NoTypeDiscriminator_Serialization() + { + var value = new PolymorphicClass_CustomConverter_NoTypeDiscriminator.DerivedClass { Number = 42 }; + string expectedJson = @"{ ""Number"" : 42 }"; + string actualJson = await Serializer.SerializeWrapper(value); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Fact] + public async Task PolymorphicClass_CustomConverter_NoTypeDiscriminator_Deserialization() + { + string json = @"{ ""Number"" : 42 }"; + PolymorphicClass_CustomConverter_NoTypeDiscriminator result = await Serializer.DeserializeWrapper(json); + Assert.Null(result); + } + + [JsonConverter(typeof(CustomConverter))] + [JsonDerivedType(typeof(DerivedClass))] + public class PolymorphicClass_CustomConverter_NoTypeDiscriminator + { + public class DerivedClass : PolymorphicClass_CustomConverter_NoTypeDiscriminator + { + public int Number { get; set; } + } + + public class CustomConverter : JsonConverter + { + public override PolymorphicClass_CustomConverter_NoTypeDiscriminator? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + reader.TrySkip(); + return null; + } + + public override void Write(Utf8JsonWriter writer, PolymorphicClass_CustomConverter_NoTypeDiscriminator value, JsonSerializerOptions options) + => writer.WriteNullValue(); + } + } + + [Theory] + [InlineData("$id")] + [InlineData("$ref")] + [InlineData("$values")] + public async Task PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName_ThrowsInvalidOperationException(string invalidPropertyName) + { + JsonSerializerOptions? options = PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName.CreatePolymorphicConfigurationWithCustomPropertyName(invalidPropertyName); + PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName value = new PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName.DerivedClass(); + await Assert.ThrowsAsync(() => Serializer.SerializeWrapper(value, options)); + } + + [Fact] + public async Task PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName_PassingDefaultPropertyNameAsCustomParameter_ShouldSucceed() + { + JsonSerializerOptions? options = PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName.CreatePolymorphicConfigurationWithCustomPropertyName("$type"); + PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName value = new PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName.DerivedClass { Number = 42 }; + + string expectedJson = @"{ ""$type"" : ""derivedClass"", ""Number"" : 42 }"; + string actualJson = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + } + + [Theory] + [InlineData(@"")] + [InlineData(@" ")] + [InlineData(@"\t")] + [InlineData(@"\r\n")] + [InlineData(@"{ ""lol"" : true }")] + public async Task PolymorphicClass_DegenerateCustomPropertyNames_ShouldSucceed(string propertyName) + { + JsonSerializerOptions? options = PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName.CreatePolymorphicConfigurationWithCustomPropertyName(propertyName); + PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName value = new PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName.DerivedClass { Number = 42 }; + + string expectedJson = @$"{{ ""{JavaScriptEncoder.Default.Encode(propertyName)}"" : ""derivedClass"", ""Number"" : 42 }}"; + string actualJson = await Serializer.SerializeWrapper(value, options); + JsonTestHelper.AssertJsonEqual(expectedJson, actualJson); + + PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName deserializeResult = await Serializer.DeserializeWrapper(actualJson, options); + Assert.IsType(deserializeResult); + } + + [JsonPolymorphic(CustomTypeDiscriminatorPropertyName = "$id")] + [JsonDerivedType(typeof(DerivedClass), "derivedClass")] + public class PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName + { + public int Number { get; set; } + + public class DerivedClass : PolymorphicClass_InvalidCustomTypeDiscriminatorPropertyName + { + } + + public static JsonSerializerOptions? CreatePolymorphicConfigurationWithCustomPropertyName(string customPropertyName) + { + if (customPropertyName == "$id") + { + // revert to attribute configuration + return null; + } + + return new JsonSerializerOptions + { + PolymorphicTypeConfigurations = + { + new JsonPolymorphicTypeConfiguration + { + CustomTypeDiscriminatorPropertyName = customPropertyName + } + .WithDerivedType("derivedClass") + } + }; + } + } + + #endregion + + #region Test Helpers + public class PolymorphicEqualityComparer : IEqualityComparer + where TBaseType : class + { + public static PolymorphicEqualityComparer Instance { get; } = new(); + + public bool Equals(TBaseType? left, TBaseType? right) + { + if (left is null || right is null) + { + return left is null == right is null; + } + + Type runtimeType = left.GetType(); + if (runtimeType != right.GetType()) + { + return false; + } + + EqualityComparer objComparer = EqualityComparer.Default; + + // Runtime type is enumerable; use enumerable sequence comparison + if (left is IEnumerable leftColl) + { + IEnumerable rightColl = (IEnumerable)right; + return leftColl.Cast().SequenceEqual(rightColl.Cast(), objComparer); + } + + // Runtime is regular POCO; use property structural comparison + foreach (var propInfo in runtimeType.GetProperties()) + { + if (!objComparer.Equals(propInfo.GetValue(left), propInfo.GetValue(right))) + { + return false; + } + } + + return true; + } + + public int GetHashCode(TBaseType _) => throw new NotImplementedException(); + } + #endregion + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs index 7a945a656ff518..40ecc53a60925b 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/PolymorphicTests.cs @@ -54,13 +54,10 @@ public class PolymorphicTests_Node : PolymorphicTests public PolymorphicTests_Node() : base(JsonSerializerWrapper.NodeSerializer) { } } - public abstract class PolymorphicTests + public abstract partial class PolymorphicTests : SerializerTests { - private JsonSerializerWrapper Serializer { get; } - - public PolymorphicTests(JsonSerializerWrapper serializer) + public PolymorphicTests(JsonSerializerWrapper serializer) : base(serializer) { - Serializer = serializer; } [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 4e22ec41f24b5a..7ff239b896384d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -22,6 +22,10 @@ + + + + @@ -163,6 +167,7 @@ + @@ -178,6 +183,7 @@ +