Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
Expand All @@ -9,6 +9,7 @@
using System.Text.Encodings.Web;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;
using Microsoft.DotNet.XUnitExtensions;
using Xunit;

namespace System.Text.Json.Serialization.Tests
Expand Down Expand Up @@ -1078,14 +1079,14 @@ public async Task NestedPolymorphicClassesIncreaseReadAndWriteStackWhenNeeded()

[JsonDerivedType(typeof(TestNodeList), "NodeList")]
[JsonDerivedType(typeof(TestLeaf), "Leaf")]
abstract class TestNode
public abstract class TestNode
{
public string Name { get; set; }

public abstract void AssertEqualTo(TestNode other);
}

class TestNodeList : TestNode
public class TestNodeList : TestNode
{
public string Info { get; set; }

Expand Down Expand Up @@ -1118,7 +1119,7 @@ public override void AssertEqualTo(TestNode other)
}
}

class TestLeaf : TestNode
public class TestLeaf : TestNode
{
public string? Test { get; set; }

Expand Down Expand Up @@ -2477,9 +2478,18 @@ public class PolymorphicClassWithoutDerivedTypeAttribute
{
}

[Fact]
[ConditionalFact]
public async Task PolymorphicClassWithNullDerivedTypeAttribute_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
// The JsonDerivedType ctor arg is dereferenced without a null check in
// JsonSourceGenerator.Parser.cs, so the generator throws NRE rather than emitting
// a SYSLIB diagnostic. Pending a generator fix, the runtime InvalidOperationException
// is validated under reflection only.
throw new SkipTestException("Generator NRE on [JsonDerivedType(null)] prevents reaching the runtime path.");
}

var value = new PolymorphicClassWithNullDerivedTypeAttribute();
await Assert.ThrowsAsync<InvalidOperationException>(() => Serializer.SerializeWrapper(value));
}
Expand Down Expand Up @@ -2656,9 +2666,14 @@ public class DerivedClass : PolymorphicGenericClass<T>
}
}

[Fact]
[ConditionalFact]
public async Task PolymorphicDerivedGenericClass_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

PolymorphicDerivedGenericClass value = new PolymorphicDerivedGenericClass.DerivedClass<int>();
await Assert.ThrowsAsync<InvalidOperationException>(() => Serializer.SerializeWrapper(value));
}
Expand Down Expand Up @@ -2842,9 +2857,14 @@ public async Task OpenGenericDerivedType_DifferentTypeArguments_ProduceDifferent
JsonTestHelper.AssertJsonEqual("""{"$type":"derived","Value":"hello"}""", strJson);
}

[Fact]
[ConditionalFact]
public async Task OpenGenericDerivedType_NonGenericBase_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

var value = new NonGenericBaseWithOpenGenericDerived();
await Assert.ThrowsAsync<InvalidOperationException>(() => Serializer.SerializeWrapper(value));
}
Expand All @@ -2855,9 +2875,14 @@ public class NonGenericBaseWithOpenGenericDerived
public class OpenDerived<T> : NonGenericBaseWithOpenGenericDerived;
}

[Fact]
[ConditionalFact]
public async Task OpenGenericDerivedType_TypeArgsNotResolvable_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

// Derived<T> : Base<int> - T cannot be determined from Base<int>
var value = new OpenGenericBase_Unresolvable<int>();
await Assert.ThrowsAsync<InvalidOperationException>(() => Serializer.SerializeWrapper(value));
Expand All @@ -2871,9 +2896,14 @@ public class OpenGenericBase_Unresolvable<T>

public class OpenGenericDerived_Unresolvable<T> : OpenGenericBase_Unresolvable<int>;

[Fact]
[ConditionalFact]
public async Task OpenGenericDerivedType_GroundMismatchAgainstClosedBase_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

// OpenGenericDerived_GroundMismatch<T> : OpenGenericBase_GroundMismatch<T, int>
// registered on OpenGenericBase_GroundMismatch<int, string>.
// Position 0 (T) unifies with int, but position 1 (concrete int in derived's base
Expand Down Expand Up @@ -3174,9 +3204,14 @@ public class OpenGenericDerived_Tuple<T1, T2> : OpenGenericBase_Tuple<(T1, T2)>
public (T1, T2) Pair { get; set; }
}

[Fact]
[ConditionalFact]
public async Task OpenGenericDerivedType_AmbiguousInterfaceMatch_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

// Impl<T> : IBase<T>, IBase<List<T>> registered on IBase<List<int>>.
// Both ancestors unify (T=List<int> via the first interface, T=int via the second).
// Result: ambiguous, throws.
Expand All @@ -3189,9 +3224,14 @@ public interface IOpenGenericBase_Ambiguous<T>;

public class OpenGenericImpl_Ambiguous<T> : IOpenGenericBase_Ambiguous<T>, IOpenGenericBase_Ambiguous<List<T>>;

[Fact]
[ConditionalFact]
public async Task OpenGenericDerivedType_UnboundParameter_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

// Derived<T1, T2> : Base<T1> — T2 is unspeakable (not bound by the base type's args).
var value = new OpenGenericBase_Unbound<int>();
await Assert.ThrowsAsync<InvalidOperationException>(() => Serializer.SerializeWrapper(value));
Expand All @@ -3202,9 +3242,14 @@ public class OpenGenericBase_Unbound<T>;

public class OpenGenericDerived_Unbound<T1, T2> : OpenGenericBase_Unbound<T1>;

[Fact]
[ConditionalFact]
public async Task OpenGenericDerivedType_ConstraintViolation_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

// Derived<T> : Base<T> where T : struct, registered on Base<string>.
// Constraint fails → InvalidOperationException.
var value = new OpenGenericBase_StructConstraint<string>();
Expand Down Expand Up @@ -3242,6 +3287,8 @@ public async Task OpenGenericDerivedType_DuplicateClosedAndOpenRegistration_Thro
// Derived<> registration. The open form closes to Derived<int>, producing a
// duplicate derived-type registration. The existing dup-detection in
// PolymorphicTypeResolver must surface this as InvalidOperationException.
// (Source generator accepts the configuration without warning; the runtime resolver
// catches the duplicate, so this scenario validates the same runtime path under both engines.)
OpenGenericBase_DuplicateDerivedRegistrations<int> value = new OpenGenericDerived_DuplicateDerivedRegistrations<int>();
await Assert.ThrowsAsync<InvalidOperationException>(() => Serializer.SerializeWrapper(value));
}
Expand Down Expand Up @@ -3491,9 +3538,14 @@ public async Task Variance_BivariantInterface_BothViaVariance_DefaultThrows()
// behavior. The reflection side has always handled these cases correctly because
// Type.GetGenericArguments() returns enclosing+leaf args together.

[Fact]
[ConditionalFact]
public async Task NestedGeneric_EnclosingMismatch_ThrowsInvalidOperationException()
{
if (Serializer.IsSourceGeneratedSerializer)
{
throw new SkipTestException("Source generator rejects this invalid polymorphic configuration at build time (SYSLIB diagnostic); the runtime InvalidOperationException is validated under reflection only.");
}

// Pattern: NestedDerivedEnclosingMismatch<T> : NestedBase<NestedOuter<int>.NestedBox<T>>.
// Target: NestedBase<NestedOuter<string>.NestedBox<int>>.
// The enclosing argument differs (int vs string) so unification MUST fail. The
Expand Down Expand Up @@ -3788,9 +3840,10 @@ public bool Equals(TBaseType? left, TBaseType? right)
public int GetHashCode(TBaseType _) => throw new NotImplementedException();
}

public class CustomPolymorphismResolver : DefaultJsonTypeInfoResolver
public class CustomPolymorphismResolver : IJsonTypeInfoResolver
{
private List<JsonDerivedType> _jsonDerivedTypes = new();
private readonly DefaultJsonTypeInfoResolver _inner = new();
private readonly List<JsonDerivedType> _jsonDerivedTypes = new();

public CustomPolymorphismResolver(Type baseType)
{
Expand All @@ -3808,10 +3861,10 @@ public CustomPolymorphismResolver WithDerivedType(JsonDerivedType jsonDerivedTyp
return this;
}

public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
{
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
if (jsonTypeInfo.Type == BaseType)
JsonTypeInfo? jsonTypeInfo = _inner.GetTypeInfo(type, options);
if (jsonTypeInfo is not null && jsonTypeInfo.Type == BaseType)
{
jsonTypeInfo.PolymorphismOptions = new()
{
Expand Down
Loading
Loading