From 9a9838da147670481af540a2241bcff13fd0e776 Mon Sep 17 00:00:00 2001 From: drken Date: Mon, 29 Jan 2024 08:34:23 +0100 Subject: [PATCH 1/4] Added KnownBaseTypeAttribute and KnownBaseTypeWithPropertyAttribute attributes. --- .../AbstractBaseClassDiscriminatorTests.cs | 33 ++ JsonSubTypes.Tests/BaseIsAnInterfaceTests.cs | 50 +++ .../DeeplyNestedDeserializationTests.cs | 36 ++ .../DemoAlternativeTypePropertyNameTests.cs | 40 ++ .../DemoCustomSubclassMappingTests.cs | 35 ++ .../DemoKnownSubTypeWithProperties.cs | 101 ++++- .../DemoKnownSubTypeWithProperty.cs | 67 +++ .../DiscriminatorOfDifferentKindTests.cs | 217 ++++++++++ JsonSubTypes.Tests/GenericTests.cs | 98 +++-- .../HiearachyWithCollectionTests.cs | 404 ++++++++++++++++++ JsonSubTypes.Tests/JsonPathFallbackTests.cs | 112 ++++- JsonSubTypes.Tests/JsonSubTypesTests.cs | 252 +++++++++++ .../MultipleHierarchyLevelsTests.cs | 71 +++ JsonSubTypes.Tests/TypePropertyCase.cs | 130 +++++- JsonSubTypes/JsonSubtypes.cs | 110 ++++- 15 files changed, 1715 insertions(+), 41 deletions(-) diff --git a/JsonSubTypes.Tests/AbstractBaseClassDiscriminatorTests.cs b/JsonSubTypes.Tests/AbstractBaseClassDiscriminatorTests.cs index 0ff5d45..bc16cfe 100644 --- a/JsonSubTypes.Tests/AbstractBaseClassDiscriminatorTests.cs +++ b/JsonSubTypes.Tests/AbstractBaseClassDiscriminatorTests.cs @@ -57,4 +57,37 @@ public void DeserializingWithAbstractClassCircleThrows() exception.Message); } } + + [TestFixture] + public class KnownBaseType_AbstractBaseClassDiscriminatorTests + { + [JsonConverter(typeof(JsonSubtypes), "Discriminator")] + [JsonSubtypes.KnownBaseType(typeof(CC), "D")] + public abstract class AA + { + } + + [JsonConverter(typeof(JsonSubtypes), "Discriminator")] + [JsonSubtypes.KnownBaseType(typeof(AA), "D")] + public abstract class BB + { + } + + [JsonConverter(typeof(JsonSubtypes), "Discriminator")] + [JsonSubtypes.KnownBaseType(typeof(BB), "D")] + public abstract class CC + { + } + + [Test] + [Timeout(2000)] + public void DeserializingWithAbstractClassCircleThrows() + { + var exception = Assert.Throws(() => + JsonConvert.DeserializeObject("{\"Discriminator\":\"D\"}")); + Assert.AreEqual( + "Could not create an instance of type JsonSubTypes.Tests.KnownBaseType_AbstractBaseClassDiscriminatorTests+AA. Type is an interface or abstract class and cannot be instantiated. Path 'Discriminator', line 1, position 17.", + exception.Message); + } + } } diff --git a/JsonSubTypes.Tests/BaseIsAnInterfaceTests.cs b/JsonSubTypes.Tests/BaseIsAnInterfaceTests.cs index 3e9c73f..aa4a263 100644 --- a/JsonSubTypes.Tests/BaseIsAnInterfaceTests.cs +++ b/JsonSubTypes.Tests/BaseIsAnInterfaceTests.cs @@ -54,4 +54,54 @@ public void UnknownMappingFails() Assert.AreEqual("Could not create an instance of type JsonSubTypes.Tests.BaseIsAnInterfaceTests+IAnimal. Type is an interface or abstract class and cannot be instantiated. Path 'Sound', line 1, position 9.", exception.Message); } } + + [TestFixture] + public class KnownBaseType_BaseIsAnInterfaceTests + { + [JsonConverter(typeof(JsonSubtypes), "Sound")] + public interface IAnimal + { + string Sound { get; } + } + + [JsonSubtypes.KnownBaseType(typeof(IAnimal), "Bark")] + public class Dog : IAnimal + { + public string Sound { get; } = "Bark"; + public string Breed { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(IAnimal), "Meow")] + public class Cat : IAnimal + { + public string Sound { get; } = "Meow"; + public bool Declawed { get; set; } + } + + [Test] + public void Test() + { + var animal = JsonConvert.DeserializeObject("{\"Sound\":\"Bark\",\"Breed\":\"Jack Russell Terrier\"}"); + Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed); + } + + [Test] + public void ConcurrentThreadTest() + { + Action test = () => + { + var animal = JsonConvert.DeserializeObject("{\"Sound\":\"Bark\",\"Breed\":\"Jack Russell Terrier\"}"); + Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed); + }; + + Parallel.For(0, 100, index => test()); + } + + [Test] + public void UnknownMappingFails() + { + var exception = Assert.Throws(() => JsonConvert.DeserializeObject("{\"Sound\":\"Scream\"}")); + Assert.AreEqual("Could not create an instance of type JsonSubTypes.Tests.KnownBaseType_BaseIsAnInterfaceTests+IAnimal. Type is an interface or abstract class and cannot be instantiated. Path 'Sound', line 1, position 9.", exception.Message); + } + } } diff --git a/JsonSubTypes.Tests/DeeplyNestedDeserializationTests.cs b/JsonSubTypes.Tests/DeeplyNestedDeserializationTests.cs index 9eadc02..ce8216f 100644 --- a/JsonSubTypes.Tests/DeeplyNestedDeserializationTests.cs +++ b/JsonSubTypes.Tests/DeeplyNestedDeserializationTests.cs @@ -38,4 +38,40 @@ public void DeserializingDeeplyNestedJsonWithHighMaxDepthParsesCorrectly() Assert.That(obj, Is.Not.Null); } } + + [TestFixture] + public class KnownBaseType_DeeplyNestedDeserializationTests + { + [JsonConverter(typeof(JsonSubtypes), nameof(SubTypeClass.Discriminator))] + public abstract class MainClass + { + } + + [JsonSubtypes.KnownBaseType(typeof(MainClass), "SubTypeClass")] + public class SubTypeClass : MainClass + { + public string Discriminator => "SubTypeClass"; + + public MainClass Child { get; set; } + } + + [Test] + public void DeserializingDeeplyNestedJsonWithHighMaxDepthParsesCorrectly() + { + var root = new SubTypeClass(); + + var current = root; + for (var i = 0; i < 64; i++) + { + var child = new SubTypeClass(); + current.Child = child; + current = child; + } + + var json = JsonConvert.SerializeObject(root); + + var obj = JsonConvert.DeserializeObject(json, new JsonSerializerSettings { MaxDepth = 65 }); + Assert.That(obj, Is.Not.Null); + } + } } diff --git a/JsonSubTypes.Tests/DemoAlternativeTypePropertyNameTests.cs b/JsonSubTypes.Tests/DemoAlternativeTypePropertyNameTests.cs index daa034b..fd2133c 100644 --- a/JsonSubTypes.Tests/DemoAlternativeTypePropertyNameTests.cs +++ b/JsonSubTypes.Tests/DemoAlternativeTypePropertyNameTests.cs @@ -253,6 +253,46 @@ public void WhenNoMappingPossible() Assert.AreEqual("Octopus", (animal as UnknownAnimal)?.Kind); } } + + [TestFixture] + public class KnownBaseType_DemoAlternativeTypePropertyNameTests + { + [JsonConverter(typeof(JsonSubtypes), "Kind")] + [JsonSubtypes.FallBackSubType(typeof(UnknownAnimal))] + public interface IAnimal + { + string Kind { get; } + } + + public class UnknownAnimal : IAnimal + { + public string Kind { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(IAnimal), null)] + public class Dog : IAnimal + { + public string Kind { get; } = null; + public string Breed { get; set; } + } + + [Test] + public void Demo() + { + var animal = + JsonConvert.DeserializeObject( + "{\"Kind\":null,\"Breed\":\"Jack Russell Terrier\"}"); + Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed); + } + + [Test] + public void WhenNoMappingPossible() + { + var animal = JsonConvert.DeserializeObject("{\"Kind\":\"Octopus\",\"Specie\":\"Octopus tetricus\"}"); + + Assert.AreEqual("Octopus", (animal as UnknownAnimal)?.Kind); + } + } } } diff --git a/JsonSubTypes.Tests/DemoCustomSubclassMappingTests.cs b/JsonSubTypes.Tests/DemoCustomSubclassMappingTests.cs index 7c9f60e..ccfe6c7 100644 --- a/JsonSubTypes.Tests/DemoCustomSubclassMappingTests.cs +++ b/JsonSubTypes.Tests/DemoCustomSubclassMappingTests.cs @@ -37,4 +37,39 @@ public void Demo() Assert.AreEqual(true, (animal as Cat)?.Declawed); } } + + [TestFixture] + public class KnownBaseType_DemoCustomSubclassMappingTests + { + [JsonConverter(typeof(JsonSubtypes), "Sound")] + public class Animal + { + public virtual string Sound { get; } + public string Color { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Animal), "Bark")] + public class Dog : Animal + { + public override string Sound { get; } = "Bark"; + public string Breed { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Animal), "Meow")] + public class Cat : Animal + { + public override string Sound { get; } = "Meow"; + public bool Declawed { get; set; } + } + + [Test] + public void Demo() + { + var animal = JsonConvert.DeserializeObject("{\"Sound\":\"Bark\",\"Breed\":\"Jack Russell Terrier\"}"); + Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed); + + animal = JsonConvert.DeserializeObject("{\"Sound\":\"Meow\",\"Declawed\":\"true\"}"); + Assert.AreEqual(true, (animal as Cat)?.Declawed); + } + } } diff --git a/JsonSubTypes.Tests/DemoKnownSubTypeWithProperties.cs b/JsonSubTypes.Tests/DemoKnownSubTypeWithProperties.cs index 56d3923..e3eeec7 100644 --- a/JsonSubTypes.Tests/DemoKnownSubTypeWithProperties.cs +++ b/JsonSubTypes.Tests/DemoKnownSubTypeWithProperties.cs @@ -71,7 +71,7 @@ public void ThrowIfManyMatches() var jsonSerializationException = Assert.Throws(() => JsonConvert.DeserializeObject(json)); Assert.AreEqual("Ambiguous type resolution, expected only one type but got: JsonSubTypes.Tests.DemoKnownSubTypeWithMultipleProperties+Employee, JsonSubTypes.Tests.DemoKnownSubTypeWithMultipleProperties+Artist", jsonSerializationException.Message); } - + [JsonConverter(typeof(JsonSubtypes))] [JsonSubtypes.KnownSubTypeWithProperty(typeof(ClassC), nameof(ClassC.Other), StopLookupOnMatch = true)] [JsonSubtypes.KnownSubTypeWithProperty(typeof(ClassB), nameof(ClassB.Optional))] @@ -97,7 +97,104 @@ public void StopLookupOnMatch() string json = "{\"CommonProp\": null, \"Optional\": null, \"Other\": null}"; ClassA deserializeObject = JsonConvert.DeserializeObject(json); - + + Assert.IsInstanceOf(deserializeObject); + } + } + + [TestFixture] + public class KnownBaseType_DemoKnownSubTypeWithMultipleProperties + { + [JsonConverter(typeof(JsonSubtypes))] + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(Person), "JobTitle")] + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(Person), "Department")] + public class Employee : Person + { + public string Department { get; set; } + public string JobTitle { get; set; } + } + + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(Person), "Skill")] + public class Artist : Person + { + public string Skill { get; set; } + } + + [Test] + public void Demo() + { + string json = "[{\"Department\":\"Department1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]"; + + + var persons = JsonConvert.DeserializeObject>(json); + Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill); + } + + [Test] + public void DemoDifferentCase() + { + string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"skill\"" + + ":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]"; + + + var persons = JsonConvert.DeserializeObject>(json); + Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill); + } + + [Test] + public void FallBackToPArentWhenNotFound() + { + string json = "[{\"Skl.\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]"; + + var persons = JsonConvert.DeserializeObject>(json); + Assert.AreEqual(typeof(Person), persons.First().GetType()); + } + + [Test] + public void ThrowIfManyMatches() + { + string json = "{\r\n \"Name\": \"Foo\",\r\n \"Skill\": \"A\",\r\n \"JobTitle\": \"B\"\r\n}"; + + var jsonSerializationException = Assert.Throws(() => JsonConvert.DeserializeObject(json)); + Assert.AreEqual("Ambiguous type resolution, expected only one type but got: JsonSubTypes.Tests.KnownBaseType_DemoKnownSubTypeWithMultipleProperties+Employee, JsonSubTypes.Tests.KnownBaseType_DemoKnownSubTypeWithMultipleProperties+Artist", jsonSerializationException.Message); + } + + [JsonConverter(typeof(JsonSubtypes))] + [JsonSubtypes.FallBackSubType(typeof(ClassB))] + public class ClassA + { + public string CommonProp { get; set; } + } + + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(ClassA), nameof(ClassB.Optional))] + public class ClassB : ClassA + { + public bool? Optional { get; set; } + } + + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(ClassA), nameof(ClassC.Other), StopLookupOnMatch = true)] + public class ClassC : ClassB + { + public string Other { get; set; } + } + + [Test] + public void StopLookupOnMatch() + { + string json = "{\"CommonProp\": null, \"Optional\": null, \"Other\": null}"; + + ClassA deserializeObject = JsonConvert.DeserializeObject(json); + Assert.IsInstanceOf(deserializeObject); } } diff --git a/JsonSubTypes.Tests/DemoKnownSubTypeWithProperty.cs b/JsonSubTypes.Tests/DemoKnownSubTypeWithProperty.cs index 6e0046e..4373a20 100644 --- a/JsonSubTypes.Tests/DemoKnownSubTypeWithProperty.cs +++ b/JsonSubTypes.Tests/DemoKnownSubTypeWithProperty.cs @@ -71,4 +71,71 @@ public void ThrowIfManyMatches() Assert.AreEqual("Ambiguous type resolution, expected only one type but got: JsonSubTypes.Tests.DemoKnownSubTypeWithProperty+Employee, JsonSubTypes.Tests.DemoKnownSubTypeWithProperty+Artist", jsonSerializationException.Message); } } + + [TestFixture] + public class KnownBaseType_DemoKnownSubTypeWithProperty + { + [JsonConverter(typeof(JsonSubtypes))] + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(Person), "JobTitle")] + public class Employee : Person + { + public string Department { get; set; } + public string JobTitle { get; set; } + } + + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(Person), "Skill")] + public class Artist : Person + { + public string Skill { get; set; } + } + + [Test] + public void Demo() + { + string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]"; + + + var persons = JsonConvert.DeserializeObject>(json); + Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill); + } + + [Test] + public void DemoDifferentCase() + { + string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," + + "{\"skill\"" + + ":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]"; + + + var persons = JsonConvert.DeserializeObject>(json); + Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill); + } + + [Test] + public void FallBackToPArentWhenNotFound() + { + string json = "[{\"Skl.\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]"; + + var persons = JsonConvert.DeserializeObject>(json); + Assert.AreEqual(typeof(Person), persons.First().GetType()); + } + + [Test] + public void ThrowIfManyMatches() + { + string json = "{\r\n \"Name\": \"Foo\",\r\n \"Skill\": \"A\",\r\n \"JobTitle\": \"B\"\r\n}"; + + var jsonSerializationException = Assert.Throws(() => JsonConvert.DeserializeObject(json)); + Assert.AreEqual("Ambiguous type resolution, expected only one type but got: JsonSubTypes.Tests.KnownBaseType_DemoKnownSubTypeWithProperty+Employee, JsonSubTypes.Tests.KnownBaseType_DemoKnownSubTypeWithProperty+Artist", jsonSerializationException.Message); + } + } } diff --git a/JsonSubTypes.Tests/DiscriminatorOfDifferentKindTests.cs b/JsonSubTypes.Tests/DiscriminatorOfDifferentKindTests.cs index 05d8b7d..987b032 100644 --- a/JsonSubTypes.Tests/DiscriminatorOfDifferentKindTests.cs +++ b/JsonSubTypes.Tests/DiscriminatorOfDifferentKindTests.cs @@ -222,4 +222,221 @@ public void Deserialize() } } + + public class KnownBaseType_DiscriminatorOfDifferentKindTests + { + [TestFixture] + public class DiscriminatorIsAnEnum + { + public class MainClass + { + public SubTypeClassBase SubTypeData { get; set; } + } + + [JsonConverter(typeof(JsonSubtypes), "SubTypeType")] + public class SubTypeClassBase + { + public SubType SubTypeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), SubType.WithAaaField)] + public class SubTypeClass1 : SubTypeClassBase + { + public string AaaField { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), SubType.WithZzzField)] + public class SubTypeClass2 : SubTypeClassBase + { + public string ZzzField { get; set; } + } + + public enum SubType + { + WithAaaField, + WithZzzField + } + + [Test] + public void Deserialize() + { + var obj = JsonConvert.DeserializeObject("{\"SubTypeData\":{\"ZzzField\":\"zzz\",\"SubTypeType\":1}}"); + Assert.AreEqual("zzz", (obj.SubTypeData as SubTypeClass2)?.ZzzField); + } + } + + [TestFixture] + public class DiscriminatorIsAnEnumStringValue + { + public class MainClass + { + public SubTypeClassBase SubTypeData { get; set; } + } + + [JsonConverter(typeof(JsonSubtypes), "SubTypeType")] + public class SubTypeClassBase + { + public SubType SubTypeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), SubType.WithAaaField)] + public class SubTypeClass1 : SubTypeClassBase + { + public string AaaField { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), SubType.WithZzzField)] + public class SubTypeClass2 : SubTypeClassBase + { + public string ZzzField { get; set; } + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum SubType + { + WithAaaField, + [EnumMember(Value = "zzzField")] + WithZzzField + } + + [Test] + public void Deserialize() + { + var obj = JsonConvert.DeserializeObject("{\"SubTypeData\":{\"ZzzField\":\"zzz\",\"SubTypeType\":\"zzzField\"}}"); + Assert.AreEqual("zzz", (obj.SubTypeData as SubTypeClass2)?.ZzzField); + } + } + + [TestFixture] + public class DiscriminatorIsAnInt + { + class Parent + { + public Child child { get; set; } + } + + [JsonConverter(typeof(JsonSubtypes), "ChildType")] + class Child + { + public virtual int ChildType { get; } + } + + [JsonSubtypes.KnownBaseType(typeof(Child), 1)] + class Child1 : Child + { + public override int ChildType { get; } = 1; + } + + [JsonSubtypes.KnownBaseType(typeof(Child), 2)] + class Child2 : Child + { + public override int ChildType { get; } = 2; + } + + [Test] + public void DiscriminatorValueCanBeANumber() + { + var root1 = JsonConvert.DeserializeObject("{\"child\":{\"ChildType\":1}}"); + var root2 = JsonConvert.DeserializeObject("{\"child\":{\"ChildType\":2}}"); + var root3 = JsonConvert.DeserializeObject("{\"child\":{\"ChildType\":8}}"); + var root4 = JsonConvert.DeserializeObject("{\"child\":{\"ChildType\":null}}"); + var root5 = JsonConvert.DeserializeObject("{\"child\":{}}"); + + Assert.NotNull(root1.child as Child1); + Assert.NotNull(root2.child as Child2); + Assert.AreEqual(typeof(Child), root3.child.GetType()); + Assert.AreEqual(typeof(Child), root4.child.GetType()); + Assert.AreEqual(typeof(Child), root5.child.GetType()); + } + } + + + [TestFixture] + public class DiscriminatorIsANullableValueType + { + public class MainClass + { + public SubTypeClassBase SubTypeData { get; set; } + } + + [JsonConverter(typeof(JsonSubtypes), "SubTypeType")] + public class SubTypeClassBase + { + public SubType? SubTypeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), null)] + public class SubTypeClass0 : SubTypeClassBase + { + public string ZeroField { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), SubType.WithAaaField)] + public class SubTypeClass1 : SubTypeClassBase + { + public string AaaField { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), SubType.WithZzzField)] + public class SubTypeClass2 : SubTypeClassBase + { + public string ZzzField { get; set; } + } + + public enum SubType + { + WithAaaField, + WithZzzField + } + + [Test] + public void Deserialize() + { + var obj = JsonConvert.DeserializeObject("{\"SubTypeData\":{\"ZzzField\":\"zzz\",\"SubTypeType\":1}}"); + Assert.AreEqual("zzz", (obj.SubTypeData as SubTypeClass2)?.ZzzField); + + obj = JsonConvert.DeserializeObject("{\"SubTypeData\":{\"ZeroField\":\"Jack\",\"SubTypeType\": null}}"); + Assert.AreEqual("Jack", (obj.SubTypeData as SubTypeClass0)?.ZeroField); + } + } + + [TestFixture] + public class DiscriminatorIsANullableRef + { + + public class MainClass + { + public SubTypeClassBase SubTypeData { get; set; } + } + + [JsonConverter(typeof(JsonSubtypes), "SubTypeType")] + public class SubTypeClassBase + { + public string SubTypeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), null)] + public class NullDiscriminatorClass : SubTypeClassBase + { + public string CrazyTypeField { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(SubTypeClassBase), "SubTypeClass1")] + public class SubTypeClass1 : SubTypeClassBase + { + public string AaaField { get; set; } + } + + [Test] + public void Deserialize() + { + var obj = JsonConvert.DeserializeObject("{\"SubTypeData\":{\"AaaField\":\"aaa\",\"SubTypeType\": \"SubTypeClass1\"}}"); + Assert.AreEqual("aaa", (obj.SubTypeData as SubTypeClass1)?.AaaField); + + obj = JsonConvert.DeserializeObject("{\"SubTypeData\":{\"CrazyTypeField\":\"Jack\",\"SubTypeType\": null}}"); + Assert.AreEqual("Jack", (obj.SubTypeData as NullDiscriminatorClass)?.CrazyTypeField); + } + } + + } } diff --git a/JsonSubTypes.Tests/GenericTests.cs b/JsonSubTypes.Tests/GenericTests.cs index 9eaf8f4..ebe76e9 100644 --- a/JsonSubTypes.Tests/GenericTests.cs +++ b/JsonSubTypes.Tests/GenericTests.cs @@ -3,47 +3,91 @@ using NUnit.Framework; namespace JsonSubTypes.Tests -{[JsonConverter(typeof(JsonSubtypes), "Type")] - [JsonSubtypes.KnownSubType(typeof(Some<>), "Some")] - public interface IResult +{ + [TestFixture] + public class GenericTests { - string Type { get;} - } + [JsonConverter(typeof(JsonSubtypes), "Type")] + [JsonSubtypes.KnownSubType(typeof(Some<>), "Some")] + public interface IResult + { + string Type { get; } + } - public class SomeInteger: Some { - public override string ResultType { get { return "SomeInteger"; }} - - } + public class SomeInteger : Some + { + public override string ResultType { get { return "SomeInteger"; } } + } - public class SomeText: Some { - public override string ResultType { get { return "SomeText"; }} - - } + public class SomeText : Some + { + public override string ResultType { get { return "SomeText"; } } + } - [JsonConverter(typeof(JsonSubtypes), "ResultType")] - //[JsonSubtypes.KnownSubType(typeof(SomeInteger), "SomeInteger")] - //[JsonSubtypes.KnownSubType(typeof(SomeText), "SomeText")] - public abstract class Some : IResult - { - - public string Type { get { return "Some"; }} - public abstract string ResultType { get; } - public T Value { get; set; } + [JsonConverter(typeof(JsonSubtypes), "ResultType")] + //[JsonSubtypes.KnownSubType(typeof(SomeInteger), "SomeInteger")] + //[JsonSubtypes.KnownSubType(typeof(SomeText), "SomeText")] + public abstract class Some : IResult + { + public string Type { get { return "Some"; } } + public abstract string ResultType { get; } + public T Value { get; set; } + } + + [Test] + public void DeserializingSubTypeWithDateParsesCorrectly() + { + var input = new SomeInteger { Value = 42 }; + var json = JsonConvert.SerializeObject(input); + + Console.WriteLine(json); + + var result = JsonConvert.DeserializeObject(json); + + Console.WriteLine(result); + } } - + [TestFixture] - public class GenericTests + public class KnownBaseType_GenericTests { - + [JsonConverter(typeof(JsonSubtypes), "Type")] + public interface IResult + { + string Type { get; } + } + + [JsonSubtypes.KnownBaseType(typeof(Some), "SomeInteger")] + public class SomeInteger : Some + { + public override string ResultType { get { return "SomeInteger"; } } + } + + [JsonSubtypes.KnownBaseType(typeof(Some), "SomeText")] + public class SomeText : Some + { + public override string ResultType { get { return "SomeText"; } } + } + + [JsonConverter(typeof(JsonSubtypes), "ResultType")] + [JsonSubtypes.KnownBaseType(typeof(IResult), "Some")] + //[JsonSubtypes.KnownSubType(typeof(SomeInteger), "SomeInteger")] + //[JsonSubtypes.KnownSubType(typeof(SomeText), "SomeText")] + public abstract class Some : IResult + { + public string Type { get { return "Some"; } } + public abstract string ResultType { get; } + public T Value { get; set; } + } [Test] public void DeserializingSubTypeWithDateParsesCorrectly() { - var input = new SomeInteger {Value = 42}; + var input = new SomeInteger { Value = 42 }; var json = JsonConvert.SerializeObject(input); Console.WriteLine(json); - + var result = JsonConvert.DeserializeObject(json); Console.WriteLine(result); diff --git a/JsonSubTypes.Tests/HiearachyWithCollectionTests.cs b/JsonSubTypes.Tests/HiearachyWithCollectionTests.cs index a4e7948..2b10c8a 100644 --- a/JsonSubTypes.Tests/HiearachyWithCollectionTests.cs +++ b/JsonSubTypes.Tests/HiearachyWithCollectionTests.cs @@ -414,4 +414,408 @@ public void DeserializeHierachyDeeperTestWithComments() } } } + + public class KnownBaseType_HiearachyWithCollectionTests + { + [TestFixture] + public class HiearachyWithListsTests + { + public class Hierachy + { + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + public Node Root { get; set; } + } + + public class Node + { + public virtual int NodeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 1)] + public class FolderNode : Node + { + public sealed override int NodeType { get; set; } = 1; + + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + public List Children { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 2)] + public class ElemNode : Node + { + public sealed override int NodeType { get; set; } = 2; + public long Size { get; set; } + } + + [Test] + public void SerializeHierachyTest() + { + var root = new Hierachy + { + Root = new FolderNode + { + Children = new List + { + new FolderNode + { + Children = new List {new ElemNode {Size = 3}} + } + } + } + }; + + var str = JsonConvert.SerializeObject(root); + + Assert.AreEqual( + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}]}}", + str); + } + + [Test] + public void DeserializeHierachyTest() + { + var input = "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + var elemNode = ((deserialized?.Root as FolderNode)?.Children.First() as ElemNode)?.Size; + Assert.AreEqual(3, elemNode); + } + + + + [Test] + public void DeserializeHierachyDeeperTest() + { + var input = + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3},{\"NodeType\":2,\"Size\":13}, null]}]}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + Assert.NotNull(deserialized); + + Assert.AreEqual(13, + ((ElemNode)((FolderNode)((FolderNode)((FolderNode)deserialized.Root).Children[0]).Children[0]) + .Children[1]).Size); + } + + [Test] + public void ConcurrentThreadDeserializeHierachyDeeperTest() + { + Action test = () => + { + var input = + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3},{\"NodeType\":2,\"Size\":13}, null]}]}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + Assert.NotNull(deserialized); + + Assert.AreEqual(13, + ((ElemNode)((FolderNode)((FolderNode)((FolderNode)deserialized.Root).Children[0]).Children[0]) + .Children[1]).Size); + }; + + Parallel.For(0, 100, index => test()); + + } + } + [TestFixture] + public class HiearachyWithArrayTests + { + public class Hierachy + { + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + public Node Root { get; set; } + } + + public class Node + { + public virtual int NodeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 1)] + public class FolderNode : Node + { + public sealed override int NodeType { get; set; } = 1; + + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + public Node[] Children { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 2)] + public class ElemNode : Node + { + public sealed override int NodeType { get; set; } = 2; + public long Size { get; set; } + } + + [Test] + public void SerializeHierachyTest() + { + var root = new Hierachy + { + Root = new FolderNode + { + Children = new[] + { + new FolderNode + { + Children = new[] {new ElemNode {Size = 3}} + } + } + } + }; + + var str = JsonConvert.SerializeObject(root); + + Assert.AreEqual( + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}]}}", + str); + } + + [Test] + public void DeserializeHierachyTest() + { + var input = "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + var elemNode = ((deserialized?.Root as FolderNode)?.Children.First() as ElemNode)?.Size; + Assert.AreEqual(3, elemNode); + } + + [Test] + public void DeserializeBadDocument() + { + var exception = Assert.Throws(() => JsonConvert.DeserializeObject("{\"Content\": []}"), "Unrecognized token: Integer"); + + Assert.AreEqual("Impossible to read JSON array to fill type: Base", exception.Message); + } + + + [Test] + public void DeserializeHierachyDeeperTest() + { + var input = + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3},{\"NodeType\":2,\"Size\":13}]}]}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + Assert.NotNull(deserialized); + + Assert.AreEqual(13, + ((ElemNode)((FolderNode)((FolderNode)((FolderNode)deserialized.Root).Children[0]).Children[0]) + .Children[1]).Size); + } + } + + [TestFixture] + public class HiearachyWithObservableCollectionTests + { + public class Hierachy + { + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + public Node Root { get; set; } + } + + public class Node + { + public virtual int NodeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 1)] + public class FolderNode : Node + { + public sealed override int NodeType { get; set; } = 1; + + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + + +#if NET35 + public List Children { get; set; } +#else + public ObservableCollection Children { get; set; } +#endif + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 2)] + public class ElemNode : Node + { + public sealed override int NodeType { get; set; } = 2; + public long Size { get; set; } + } + + [Test] + public void SerializeHierachyTest() + { + var root = new Hierachy + { + Root = new FolderNode + { + +#if NET35 + Children = new List +#else + Children = new ObservableCollection +#endif + + { + new FolderNode + { +#if NET35 + Children = new List {new ElemNode {Size = 3}} +#else + Children = new ObservableCollection {new ElemNode {Size = 3}} +#endif + + } + } + } + }; + + var str = JsonConvert.SerializeObject(root); + + Assert.AreEqual( + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}]}}", + str); + } + + [Test] + public void DeserializeHierachyTest() + { + var input = "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + var elemNode = ((deserialized?.Root as FolderNode)?.Children.First() as ElemNode)?.Size; + Assert.AreEqual(3, elemNode); + } + + [Test] + public void DeserializeHierachyDeeperTest() + { + var input = + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3},{\"NodeType\":2,\"Size\":13}]}]}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + Assert.NotNull(deserialized); + + Assert.AreEqual(13, + ((ElemNode)((FolderNode)((FolderNode)((FolderNode)deserialized.Root).Children[0]).Children[0]) + .Children[1]).Size); + } + } + [TestFixture] + public class HiearachyWithIEnumerableCollectionTests + { + public class Hierachy + { + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + public Node Root { get; set; } + } + + public class Node + { + public virtual int NodeType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 1)] + public class FolderNode : Node + { + public sealed override int NodeType { get; set; } = 1; + + [JsonConverter(typeof(JsonSubtypes), "NodeType")] + public IEnumerable Children { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(Node), 2)] + public class ElemNode : Node + { + public sealed override int NodeType { get; set; } = 2; + public long Size { get; set; } + } + + [Test] + public void SerializeHierachyTest() + { + var root = new Hierachy + { + Root = new FolderNode + { +#if NET35 + Children = new List +#else + Children = new ObservableCollection +#endif + { + new FolderNode + { +#if NET35 + Children = new List {new ElemNode {Size = 3}} +#else + Children = new ObservableCollection {new ElemNode {Size = 3}} +#endif + + } + } + } + }; + + var str = JsonConvert.SerializeObject(root); + + Assert.AreEqual( + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}]}}", + str); + } + + [Test] + public void DeserializeHierachyTest() + { + var input = "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + var elemNode = ((deserialized?.Root as FolderNode)?.Children.First() as ElemNode)?.Size; + Assert.AreEqual(3, elemNode); + } + + [Test] + public void DeserializeHierachyDeeperTest() + { + var input = + "{\"Root\":{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":1,\"Children\":[{\"NodeType\":2,\"Size\":3},{\"NodeType\":2,\"Size\":13}]}]}]}}"; + + var deserialized = JsonConvert.DeserializeObject(input); + + Assert.NotNull(deserialized); + + Assert.AreEqual(13, + ((ElemNode)((FolderNode)((FolderNode)((FolderNode)deserialized.Root).Children.First()).Children + .First()).Children.Skip(1).First()).Size); + } + + [Test] + public void DeserializeHierachyDeeperTestWithComments() + { + var input = "/* foo bar */{/* foo bar */\"Root\":" + + "/* foo bar */ /* foo bar */ {/* foo bar */\"NodeType\":1,/* foo bar */\"Children\":" + + "/* foo bar */[/* foo bar */{\"NodeType\":1,/* foo bar */\"Children\":" + + "/* foo bar */[/* foo bar */{\"NodeType\":1,/* foo bar */\"Children\":" + + "/* foo bar */[" + + "/* foo bar */{/* foo bar */\"NodeType\":2,\"Size\":3}/* foo bar */," + + "/* foo bar */{/* foo bar */\"NodeType\":2,\"Size\":13}/* foo bar */]" + + "/* foo bar */}/* foo bar */]/* foo bar */}/* foo bar */]/* foo bar */}/* foo bar */}/* foo bar */"; + + var deserialized = JsonConvert.DeserializeObject(input); + + Assert.NotNull(deserialized); + + Assert.AreEqual(13, + ((ElemNode)((FolderNode)((FolderNode)((FolderNode)deserialized.Root).Children.First()).Children + .First()).Children.Skip(1).First()).Size); + } + } + } } diff --git a/JsonSubTypes.Tests/JsonPathFallbackTests.cs b/JsonSubTypes.Tests/JsonPathFallbackTests.cs index 213b0ca..5a6c4f1 100644 --- a/JsonSubTypes.Tests/JsonPathFallbackTests.cs +++ b/JsonSubTypes.Tests/JsonPathFallbackTests.cs @@ -11,7 +11,6 @@ class Nested: Main [JsonConverter(typeof(JsonSubtypes))] [JsonSubtypes.KnownSubTypeWithProperty(typeof(DottedSub), "nested.property")] [JsonSubtypes.KnownSubTypeWithProperty(typeof(Nested), "nested.otherproperty")] - class Main { @@ -28,7 +27,6 @@ class DottedSub : Main string property { get; set; } } - class NestedClass { @@ -113,3 +111,113 @@ public void CheckDottedDiscriminator() } } } + +namespace JsonSubTypes.Tests.JsonPath.KnownBaseType +{ + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(Main), "nested.otherproperty")] + class Nested : Main + { + string otherproperty { get; set; } + } + + [JsonConverter(typeof(JsonSubtypes))] + class Main + { + + } + + class Sub : Main + { + Nested nested { get; set; } + } + + [JsonSubtypes.KnownBaseTypeWithProperty(typeof(Main), "nested.property")] + class DottedSub : Main + { + [JsonProperty("nested.property")] + string property { get; set; } + } + + class NestedClass + { + + } + + [JsonConverter(typeof(JsonSubtypes), "nested.property")] + class MainDiscriminator + { + public NestedClass nested; + } + + class SubNestedClass : NestedClass + { + string property = "SubNestedClass"; + } + + class OtherNestedClass : NestedClass + { + string property = "OtherNestedClass"; + } + + [JsonSubtypes.KnownBaseType(typeof(MainDiscriminator), "SubNestedClass")] + class SubDiscriminator : MainDiscriminator + { + new SubNestedClass nested { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(MainDiscriminator), "OtherNestedClass")] + class OtherDiscriminator : MainDiscriminator + { + new OtherNestedClass nested { get; set; } + } + + + [JsonConverter(typeof(JsonSubtypes), "dotted.property")] + class MainDottedDiscriminator + { + [JsonProperty("dotted.property")] + string property { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(MainDottedDiscriminator), "SubNestedClass")] + class SubDottedDiscriminator : MainDottedDiscriminator + { + + } + + [TestFixture] + public class JsonPathFallbackTests + { + [Test] + public void CheckDottedProperty() + { + string json = "{\"nested.property\": \"str\"}"; + var result = JsonConvert.DeserializeObject
(json); + Assert.IsInstanceOf(result); + } + + [Test] + public void CheckNestedProperty() + { + string json = "{nested: { otherproperty: \"abc\" } }"; + var result = JsonConvert.DeserializeObject
(json); + Assert.IsInstanceOf(result); + } + + [Test] + public void CheckNestedDiscriminator() + { + string json = "{nested: { property: \"SubNestedClass\" } }"; + var result = JsonConvert.DeserializeObject(json); + Assert.IsInstanceOf(result); + } + + [Test] + public void CheckDottedDiscriminator() + { + string json = "{\"dotted.property\": \"SubNestedClass\"}"; + var result = JsonConvert.DeserializeObject(json); + Assert.IsInstanceOf(result); + } + } +} diff --git a/JsonSubTypes.Tests/JsonSubTypesTests.cs b/JsonSubTypes.Tests/JsonSubTypesTests.cs index 102b837..50cd5b2 100644 --- a/JsonSubTypes.Tests/JsonSubTypesTests.cs +++ b/JsonSubTypes.Tests/JsonSubTypesTests.cs @@ -255,3 +255,255 @@ public void RefuseToWrite() } } } + +namespace JsonSubTypes.Tests.KnownBaseType +{ + class Root + { + public Base Content { get; set; } + public List ContentList { get; set; } + + protected bool Equals(Root other) + { + if (Equals(Content, other.Content)) + { + return ContentList == null || other.ContentList == null + ? ReferenceEquals(ContentList, other.ContentList) + : ContentList.SequenceEqual(other.ContentList); + } + return false; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((Root)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Content != null ? Content.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ (ContentList != null + ? ContentList.Aggregate(0, (x, y) => x.GetHashCode() ^ y.GetHashCode()) + : 0); + return hashCode; + } + } + } + + [JsonConverter(typeof(JsonSubtypes), "@type")] + class Base + { + [JsonProperty("@type")] + public virtual string Type { get; } + + [JsonProperty("4-you")] + public int _4You { get; set; } + + protected bool Equals(Base other) + { + return string.Equals(Type, other.Type) && _4You == other._4You; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is Base && Equals((Base)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Type != null ? Type.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ _4You; + return hashCode; + } + } + } + + [JsonSubtypes.KnownBaseType(typeof(Base), "SubB")] + class SubB : Base + { + public override string Type { get; } = "SubB"; + public int Index { get; set; } + + protected bool Equals(SubB other) + { + return base.Equals(other) && Index == other.Index; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is SubB && Equals((SubB)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ Index; + } + } + } + + [JsonSubtypes.KnownBaseType(typeof(Base), "SubC")] + class SubC : Base + { + public override string Type { get; } = "SubC"; + + public string Name { get; set; } + + protected bool Equals(SubC other) + { + return base.Equals(other) && string.Equals(Name, other.Name); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is SubC && Equals((SubC)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (base.GetHashCode() * 397) ^ (Name != null ? Name.GetHashCode() : 0); + } + } + } + [TestFixture] + public class JsonSubTypesTests + { + [Test] + public void SerializeTest() + { + var root = new Root + { + Content = new SubB + { + Index = 1, + _4You = 2 + } + }; + + string str = JsonConvert.SerializeObject(root); + + Assert.AreEqual("{\"Content\":{\"@type\":\"SubB\",\"Index\":1,\"4-you\":2},\"ContentList\":null}", str); + } + + [Test] + public void DeserializeSubType() + { + var expected = new Root + { + Content = new SubB { Index = 1 } + }; + + var root = JsonConvert.DeserializeObject("{\"Content\":{\"Index\":1,\"@type\":\"SubB\"}}"); + + Assert.AreEqual(expected, root); + } + + [Test] + public void DeserializeSubTypeWithComments() + { + var expected = new Root + { + Content = new SubB { Index = 1 } + }; + + var root = JsonConvert.DeserializeObject( + "{\"Content\":/* foo bar */{\"Index\":1,\"@type\":\"SubB\"}}"); + + Assert.AreEqual(expected, root); + } + + [Test] + public void DeserializeNull() + { + var expected = new Root + { + Content = null + }; + + var root = JsonConvert.DeserializeObject("{\"Content\":null}"); + + Assert.AreEqual(expected, root); + } + + [Test] + public void DeserializeBadDocument() + { + var exception = Assert.Throws(() => JsonConvert.DeserializeObject("{\"Content\":8}")); + + Assert.AreEqual("Unrecognized token: Integer", exception.Message); + } + + [Test] + public void WhenDiscriminatorValueIsNullDeserializeToBaseType() + { + var expected = new Root + { + Content = new Base() + }; + + var root = JsonConvert.DeserializeObject("{\"Content\":{\"Index\":1,\"@type\":null}}"); + + Assert.AreEqual(expected, root); + } + + [Test] + public void WhenDiscriminatorValueIsUnknownDeserializeToBaseType() + { + var expected = new Root + { + Content = new Base() + }; + + var root = JsonConvert.DeserializeObject("{\"Content\":{\"Index\":1,\"@type\":8.5}}"); + + Assert.AreEqual(expected, root); + } + + [Test] + public void WorkWithSubList() + { + var expected = new Root + { + Content = new Base(), + ContentList = new List { new SubB { Index = 1 }, new SubC { Name = "foo" } } + }; + + var root = JsonConvert.DeserializeObject( + "{\"Content\":{\"Index\":1,\"@type\":8.5},\"ContentList\":[{\"Index\":1,\"@type\":\"SubB\"},{\"Name\":\"foo\",\"@type\":\"SubC\"}]}"); + + Assert.AreEqual(expected, root); + } + + [Test] + public void CouldNotBeUsedAsRegisteredConverter() + { + var converter = new JsonSubtypes(); + Assert.False(converter.CanConvert(typeof(SubB))); + Assert.False(converter.CanConvert(typeof(Base))); + } + + [Test] + public void RefuseToWrite() + { + var converter = new JsonSubtypes(); + Assert.False(converter.CanWrite); + Assert.Throws(() => converter.WriteJson(null, null, null)); + } + } +} diff --git a/JsonSubTypes.Tests/MultipleHierarchyLevelsTests.cs b/JsonSubTypes.Tests/MultipleHierarchyLevelsTests.cs index f6725be..a66aae4 100644 --- a/JsonSubTypes.Tests/MultipleHierarchyLevelsTests.cs +++ b/JsonSubTypes.Tests/MultipleHierarchyLevelsTests.cs @@ -305,3 +305,74 @@ public class Ride : Game } } } + +namespace JsonSubTypes.Tests.KnownBaseType +{ + [TestFixture] + public class MultipleHierarchyLevelsTests + { + [Test] + public void ShouldDeserializeNestedLevel() + { + var data = "{\"$GameKind\":0,\"$PayloadKind\":1}"; + Assert.IsInstanceOf(JsonConvert.DeserializeObject(data)); + } + + [Test] + public void ShouldSerializeNestedLevel() + { + Payload run = new Run(); + var data = JsonConvert.SerializeObject(run); + Assert.AreEqual("{\"$GameKind\":0,\"$PayloadKind\":1}", data); + } + + public enum PayloadDiscriminator + { + COM = 0, + GAME = 1 + } + + public enum GameDiscriminator + { + RUN = 0, + WALK = 1 + } + + [JsonConverter(typeof(JsonSubtypes), PAYLOAD_KIND)] + public abstract class Payload + { + public const string PAYLOAD_KIND = "$PayloadKind"; + + [JsonProperty(PAYLOAD_KIND)] public abstract PayloadDiscriminator PayloadKind { get; } + } + + [JsonConverter(typeof(JsonSubtypes), GAME_KIND)] + [JsonSubtypes.KnownBaseType(typeof(Payload), PayloadDiscriminator.GAME)] + public abstract class Game : Payload + { + public override PayloadDiscriminator PayloadKind => PayloadDiscriminator.GAME; + + public const string GAME_KIND = "$GameKind"; + + [JsonProperty(GAME_KIND)] public abstract GameDiscriminator GameKind { get; } + } + + [JsonSubtypes.KnownBaseType(typeof(Payload), PayloadDiscriminator.COM)] + public class Com : Payload + { + public override PayloadDiscriminator PayloadKind => PayloadDiscriminator.COM; + } + + [JsonSubtypes.KnownBaseType(typeof(Game), GameDiscriminator.WALK)] + public class Walk : Game + { + public override GameDiscriminator GameKind => GameDiscriminator.WALK; + } + + [JsonSubtypes.KnownBaseType(typeof(Game), GameDiscriminator.RUN)] + public class Run : Game + { + public override GameDiscriminator GameKind => GameDiscriminator.RUN; + } + } +} diff --git a/JsonSubTypes.Tests/TypePropertyCase.cs b/JsonSubTypes.Tests/TypePropertyCase.cs index 3df1ceb..8e01ba7 100644 --- a/JsonSubTypes.Tests/TypePropertyCase.cs +++ b/JsonSubTypes.Tests/TypePropertyCase.cs @@ -4,7 +4,6 @@ namespace JsonSubTypes.Tests { - public class TypePropertyCase { public class TypePropertyCase_LowerWithHigher @@ -131,4 +130,131 @@ public void FooParsingLowerPascalCase() } } } -} \ No newline at end of file + + public class KnownBaseType_TypePropertyCase + { + public class TypePropertyCase_LowerWithHigher + { + [JsonConverter(typeof(JsonSubtypes), "msgType")] + public abstract class DtoBase + { + public virtual int MsgType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(DtoBase), 1)] + class Foo : DtoBase + { + } + + [Test] + public void FooParsingCamelCase() + { + var serializeObject = "{\"MsgType\":1}"; + var msgType = JsonConvert.DeserializeObject(serializeObject).MsgType; + Assert.AreEqual(1, msgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + + [Test] + public void FooParsingLowerPascalCase() + { + var serializeObject = "{\"msgType\":1}"; + Assert.AreEqual(1, JsonConvert.DeserializeObject(serializeObject).MsgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + } + + public class TypePropertyCase_HigherWithLower + { + [JsonConverter(typeof(JsonSubtypes), "MsgType")] + public abstract class DtoBase + { + [JsonProperty("msgType")] + public virtual int MsgType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(DtoBase), 1)] + class Foo : DtoBase + { + } + + [Test] + public void FooParsingCamelCase() + { + var serializeObject = "{\"MsgType\":1}"; + Assert.AreEqual(1, JsonConvert.DeserializeObject(serializeObject).MsgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + + [Test] + public void FooParsingLowerPascalCase() + { + var serializeObject = "{\"msgType\":1}"; + Assert.AreEqual(1, JsonConvert.DeserializeObject(serializeObject).MsgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + } + + public class TypePropertyCase_RedirectLowerWithHigher + { + [JsonConverter(typeof(JsonSubtypes), "messageType")] + public abstract class DtoBase + { + [JsonProperty("MessageType")] + public virtual int MsgType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(DtoBase), 1)] + class Foo : DtoBase + { + } + + [Test] + public void FooParsingCamelCase() + { + var serializeObject = "{\"MessageType\":1}"; + Assert.AreEqual(1, JsonConvert.DeserializeObject(serializeObject).MsgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + + [Test] + public void FooParsingLowerPascalCase() + { + var serializeObject = "{\"messageType\":1}"; + Assert.AreEqual(1, JsonConvert.DeserializeObject(serializeObject).MsgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + } + + public class TypePropertyCase_RedirectHigherWithLower + { + [JsonConverter(typeof(JsonSubtypes), "MessageType")] + public abstract class DtoBase + { + [JsonProperty("messageType")] + public virtual int MsgType { get; set; } + } + + [JsonSubtypes.KnownBaseType(typeof(DtoBase), 1)] + class Foo : DtoBase + { + } + + [Test] + public void FooParsingCamelCase() + { + var serializeObject = "{\"MessageType\":1}"; + Assert.AreEqual(1, JsonConvert.DeserializeObject(serializeObject).MsgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + + [Test] + public void FooParsingLowerPascalCase() + { + var serializeObject = "{\"messageType\":1}"; + Assert.AreEqual(1, JsonConvert.DeserializeObject(serializeObject).MsgType); + Assert.IsInstanceOf(JsonConvert.DeserializeObject(serializeObject)); + } + } + } +} diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index 41b04b1..f53dbf3 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -53,6 +53,19 @@ public KnownSubTypeAttribute(Type subType, object associatedValue) } } + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)] + public class KnownBaseTypeAttribute : Attribute + { + public Type BaseType { get; } + public object AssociatedValue { get; } + + public KnownBaseTypeAttribute(Type baseType, object associatedValue) + { + BaseType = baseType; + AssociatedValue = associatedValue; + } + } + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] public class FallBackSubTypeAttribute : Attribute { @@ -78,6 +91,20 @@ public KnownSubTypeWithPropertyAttribute(Type subType, string propertyName) } } + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)] + public class KnownBaseTypeWithPropertyAttribute : Attribute + { + public Type BaseType { get; } + public string PropertyName { get; } + public bool StopLookupOnMatch { get; set; } + + public KnownBaseTypeWithPropertyAttribute(Type baseType, string propertyName) + { + BaseType = baseType; + PropertyName = propertyName; + } + } + protected readonly string JsonDiscriminatorPropertyName; [ThreadStatic] private static bool _isInsideRead; @@ -86,9 +113,11 @@ public KnownSubTypeWithPropertyAttribute(Type subType, string propertyName) #if NET35 private static readonly Dictionary> _attributesCache = new Dictionary>(); + private static readonly Dictionary> _typesWithKnownBaseTypeAttributesCache = new Dictionary>(); #else private static readonly ConcurrentDictionary> _attributesCache = new ConcurrentDictionary>(); private static readonly Func> _getCustomAttributes = ti => ti.GetCustomAttributes(false); + private static readonly ConcurrentDictionary> _typesWithKnownBaseTypeAttributesCache = new ConcurrentDictionary>(); #endif public override bool CanRead @@ -144,16 +173,16 @@ private object ReadJson(JsonReader reader, Type objectType, JsonSerializer seria value = ReadObject(reader, objectType, serializer); break; case JsonToken.StartArray: - { - var elementType = GetElementType(objectType); - if (elementType == null) { - throw CreateJsonReaderException(reader, $"Impossible to read JSON array to fill type: {objectType.Name}"); + var elementType = GetElementType(objectType); + if (elementType == null) + { + throw CreateJsonReaderException(reader, $"Impossible to read JSON array to fill type: {objectType.Name}"); + } + + value = ReadArray(reader, objectType, elementType, serializer); + break; } - - value = ReadArray(reader, objectType, elementType, serializer); - break; - } default: throw CreateJsonReaderException(reader, $"Unrecognized token: {reader.TokenType}"); } @@ -342,6 +371,17 @@ internal virtual List GetTypesByPropertyPres { return GetAttributes(ToTypeInfo(parentType)) .Select(a => new TypeWithPropertyMatchingAttributes(a.SubType, a.PropertyName, a.StopLookupOnMatch)) + .Concat( + FindTypesWithAttribute(ToTypeInfo(typeof(KnownBaseTypeWithPropertyAttribute))) + .Where(x => GetAttributes(ToTypeInfo(x)).Any(t => t.BaseType == parentType)) + .Select(x => + { + var firstAttribute = GetAttributes(ToTypeInfo(x)) + .Where(t => t.BaseType == parentType) + .First(); + return new TypeWithPropertyMatchingAttributes(x, firstAttribute.PropertyName, firstAttribute.StopLookupOnMatch); + }) + ) .ToList(); } @@ -463,6 +503,19 @@ internal virtual NullableDictionary GetSubTypeMapping(Type type) .ToList() .ForEach(x => dictionary.Add(x.AssociatedValue, x.SubType)); + FindTypesWithAttribute(ToTypeInfo(typeof(KnownBaseTypeAttribute))) + .Where(x => GetAttributes(ToTypeInfo(x)).Any(t => t.BaseType == type)) + .Select(x => new + { + AssociatedValue = GetAttributes(ToTypeInfo(x)) + .Where(t => t.BaseType == type) + .Select(t => t.AssociatedValue) + .First(), + SubType = x + }) + .ToList() + .ForEach(x => dictionary.Add(x.AssociatedValue, x.SubType)); + return dictionary; } @@ -514,6 +567,47 @@ private static T GetAttribute(TypeInfo typeInfo) where T : Attribute return GetAttributes(typeInfo).FirstOrDefault(); } + private static IEnumerable FindTypesWithAttribute(TypeInfo attributeType) + { + IEnumerable _getTypesWithCustomAttribute() + { +#if NETSTANDARD1_3 + return new Type[0]; +#else + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (var assembly in assemblies) + { + // Get types from the assembly + var types = assembly.GetTypes(); + + // Filter types based on the presence of the attribute and its property value + var filteredTypes = types + .Where(t => t.GetCustomAttributes(attributeType, false).Any()); + + foreach (var t in filteredTypes) + { + yield return t; + } + } +#endif + }; + +#if NET35 + lock (_attributesCache) + { + if (_typesWithKnownBaseTypeAttributesCache.TryGetValue(attributeType, out var res)) + return res; + + res = _getTypesWithCustomAttribute(); + _typesWithKnownBaseTypeAttributesCache.Add(attributeType, res); + + return res; + } +#else + return _typesWithKnownBaseTypeAttributesCache.GetOrAdd(attributeType, _getTypesWithCustomAttribute()); +#endif + } + private static IEnumerable GetGenericTypeArguments(Type type) { #if (NET35 || NET40) From fe52f6a3a22f856dd99395085f849df615bd9bfd Mon Sep 17 00:00:00 2001 From: drken Date: Mon, 29 Jan 2024 08:50:53 +0100 Subject: [PATCH 2/4] Removed unnecessary semicolon. --- JsonSubTypes/JsonSubtypes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index f53dbf3..d07ff76 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -590,7 +590,7 @@ IEnumerable _getTypesWithCustomAttribute() } } #endif - }; + } #if NET35 lock (_attributesCache) From 480184c8725e63fc9f0a2aeefa3f5ac29e333e4c Mon Sep 17 00:00:00 2001 From: drken Date: Thu, 1 Feb 2024 14:06:34 +0100 Subject: [PATCH 3/4] Catch ReflectionTypeLoadException while loading all the Types in all of the assemblies --- JsonSubTypes/JsonSubtypes.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index d07ff76..992f1ed 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -577,10 +577,19 @@ IEnumerable _getTypesWithCustomAttribute() var assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (var assembly in assemblies) { - // Get types from the assembly - var types = assembly.GetTypes(); - - // Filter types based on the presence of the attribute and its property value + TypeInfo[] types; + try + { + types = assembly.GetTypes(); + } + //For example: Microsoft.Graph, combined with PnP.Framework can throw these. + //In my testing, combining PnP.Framework v1.8.0 and Microsoft.Graph v5.38.0 gives the following Exception: + //System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types. + //Could not load type 'Microsoft.Graph.HttpProvider' from assembly 'Microsoft.Graph.Core, Version=3.1.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. + catch (System.Reflection.ReflectionTypeLoadException) + { + continue; + } var filteredTypes = types .Where(t => t.GetCustomAttributes(attributeType, false).Any()); @@ -598,7 +607,7 @@ IEnumerable _getTypesWithCustomAttribute() if (_typesWithKnownBaseTypeAttributesCache.TryGetValue(attributeType, out var res)) return res; - res = _getTypesWithCustomAttribute(); + res = _getTypesWithCustomAttribute(); _typesWithKnownBaseTypeAttributesCache.Add(attributeType, res); return res; From 839df4193ecb18b539600695ef0751e043b84c15 Mon Sep 17 00:00:00 2001 From: aldrashan Date: Wed, 6 Mar 2024 13:35:23 +0100 Subject: [PATCH 4/4] Fixed performance breaking bug Caching should now work as expected --- JsonSubTypes/JsonSubtypes.cs | 86 ++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/JsonSubTypes/JsonSubtypes.cs b/JsonSubTypes/JsonSubtypes.cs index 992f1ed..7d900bb 100644 --- a/JsonSubTypes/JsonSubtypes.cs +++ b/JsonSubTypes/JsonSubtypes.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -133,6 +133,7 @@ public override bool CanRead public override bool CanWrite => false; + private static readonly TypeInfo _knownBaseTypeAttributeType = ToTypeInfo(typeof(KnownBaseTypeAttribute)); public JsonSubtypes() { } @@ -499,23 +500,25 @@ internal virtual NullableDictionary GetSubTypeMapping(Type type) { var dictionary = new NullableDictionary(); - GetAttributes(ToTypeInfo(type)) - .ToList() - .ForEach(x => dictionary.Add(x.AssociatedValue, x.SubType)); + var typeAsTypeInfo = ToTypeInfo(type); - FindTypesWithAttribute(ToTypeInfo(typeof(KnownBaseTypeAttribute))) - .Where(x => GetAttributes(ToTypeInfo(x)).Any(t => t.BaseType == type)) - .Select(x => new - { - AssociatedValue = GetAttributes(ToTypeInfo(x)) - .Where(t => t.BaseType == type) - .Select(t => t.AssociatedValue) - .First(), - SubType = x - }) + GetAttributes(typeAsTypeInfo) .ToList() .ForEach(x => dictionary.Add(x.AssociatedValue, x.SubType)); + FindTypesWithAttribute(_knownBaseTypeAttributeType) + .Where(x => GetAttributes(typeAsTypeInfo).Any(t => t.BaseType == type)) + .Select(x => new + { + AssociatedValue = GetAttributes(typeAsTypeInfo) + .Where(t => t.BaseType == type) + .Select(t => t.AssociatedValue) + .First(), + SubType = x + }) + .ToList() + .ForEach(x => dictionary.Add(x.AssociatedValue, x.SubType)); + return dictionary; } @@ -567,53 +570,52 @@ private static T GetAttribute(TypeInfo typeInfo) where T : Attribute return GetAttributes(typeInfo).FirstOrDefault(); } - private static IEnumerable FindTypesWithAttribute(TypeInfo attributeType) + private static IEnumerable _getTypesWithCustomAttribute(TypeInfo attributeType) { - IEnumerable _getTypesWithCustomAttribute() - { #if NETSTANDARD1_3 return new Type[0]; #else - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - foreach (var assembly in assemblies) + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + List> typesInAssemblies = new List>(); + foreach (var assembly in assemblies) + { + IEnumerable types; + try { - TypeInfo[] types; - try - { - types = assembly.GetTypes(); - } - //For example: Microsoft.Graph, combined with PnP.Framework can throw these. - //In my testing, combining PnP.Framework v1.8.0 and Microsoft.Graph v5.38.0 gives the following Exception: - //System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types. - //Could not load type 'Microsoft.Graph.HttpProvider' from assembly 'Microsoft.Graph.Core, Version=3.1.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. - catch (System.Reflection.ReflectionTypeLoadException) - { - continue; - } - var filteredTypes = types - .Where(t => t.GetCustomAttributes(attributeType, false).Any()); - - foreach (var t in filteredTypes) - { - yield return t; - } + types = assembly.GetTypes(); } -#endif + //For example: Microsoft.Graph, combined with PnP.Framework can throw these. + //In my testing, combining PnP.Framework v1.8.0 and Microsoft.Graph v5.38.0 gives the following Exception: + //System.Reflection.ReflectionTypeLoadException: Unable to load one or more of the requested types. + //Could not load type 'Microsoft.Graph.HttpProvider' from assembly 'Microsoft.Graph.Core, Version=3.1.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. + catch (System.Reflection.ReflectionTypeLoadException e) + { + types = e.Types.Where(x => x != null); + } + var filteredTypes = types + .Where(t => t.GetCustomAttributes(attributeType, false).Any()); + + typesInAssemblies.Add(filteredTypes); } + return typesInAssemblies.SelectMany(x => x).ToList(); +#endif + } + private static IEnumerable FindTypesWithAttribute(TypeInfo attributeType) + { #if NET35 lock (_attributesCache) { if (_typesWithKnownBaseTypeAttributesCache.TryGetValue(attributeType, out var res)) return res; - res = _getTypesWithCustomAttribute(); + res = _getTypesWithCustomAttribute(attributeType); _typesWithKnownBaseTypeAttributesCache.Add(attributeType, res); return res; } #else - return _typesWithKnownBaseTypeAttributesCache.GetOrAdd(attributeType, _getTypesWithCustomAttribute()); + return _typesWithKnownBaseTypeAttributesCache.GetOrAdd(attributeType, _getTypesWithCustomAttribute); #endif }