Skip to content

Commit

Permalink
Merge pull request #151 from manuc66/feature/Allow_to_disable_behavior
Browse files Browse the repository at this point in the history
Allow to StopLookupOnMatch #128
  • Loading branch information
manuc66 authored Sep 10, 2022
2 parents 62d53de + 481bd37 commit 87abcbf
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 37 deletions.
29 changes: 29 additions & 0 deletions JsonSubTypes.Tests/DemoKnownSubTypeWithProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,34 @@ public void ThrowIfManyMatches()
var jsonSerializationException = Assert.Throws<JsonSerializationException>(() => JsonConvert.DeserializeObject<Person>(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))]
[JsonSubtypes.FallBackSubType(typeof(ClassB))]
public class ClassA
{
public string CommonProp { get; set; }
}

public class ClassB : ClassA
{
public bool? Optional { get; set; }
}

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<ClassA>(json);

Assert.IsInstanceOf<ClassC>(deserializeObject);
}
}
}
75 changes: 45 additions & 30 deletions JsonSubTypes/JsonSubtypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Newtonsoft.Json.Linq;
#if (!NETSTANDARD1_3)
using TypeInfo = System.Type;

#else
using System.Reflection;
#endif
Expand Down Expand Up @@ -68,6 +69,7 @@ public class KnownSubTypeWithPropertyAttribute : Attribute
{
public Type SubType { get; }
public string PropertyName { get; }
public bool StopLookupOnMatch { get; set; }

public KnownSubTypeWithPropertyAttribute(Type subType, string propertyName)
{
Expand Down Expand Up @@ -142,15 +144,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)
{
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;
throw CreateJsonReaderException(reader, $"Impossible to read JSON array to fill type: {objectType.Name}");
}

value = ReadArray(reader, objectType, elementType, serializer);
break;
}
default:
throw CreateJsonReaderException(reader, $"Unrecognized token: {reader.TokenType}");
}
Expand Down Expand Up @@ -297,42 +300,51 @@ private Type GetTypeByPropertyPresence(JObject jObject, Type parentType)
{
var knownSubTypeAttributes = GetTypesByPropertyPresence(parentType);

var types = knownSubTypeAttributes
.Select(knownType =>
HashSet<Type> typesFound = new HashSet<Type>();
foreach (TypeWithPropertyMatchingAttributes knownTypeItem in knownSubTypeAttributes)
{
Type matchingKnownType = null;
if (TryGetValueInJson(jObject, knownTypeItem.JsonPropertyName, out JToken _))
{
if (TryGetValueInJson(jObject, knownType.Key, out JToken _))
return knownType.Value;

var token = jObject.SelectToken(knownType.Key);
matchingKnownType = knownTypeItem.Type;
}
else
{
JToken token = jObject.SelectToken(knownTypeItem.JsonPropertyName);
if (token != null)
{
return knownType.Value;
matchingKnownType = knownTypeItem.Type;
}
}

return null;
})
.Where(type => type != null)
.ToArray();

var distinctTypes = types.Distinct().Count();
if (matchingKnownType != null)
{
if (knownTypeItem.StopLookupOnMatch)
{
return knownTypeItem.Type;
}
typesFound.Add(matchingKnownType);
}
}

if (distinctTypes == 1)
if (typesFound.Count == 1)
{
return types[0];
return typesFound.First();
}

if (distinctTypes > 1)
if (typesFound.Count > 1)
{
throw new JsonSerializationException("Ambiguous type resolution, expected only one type but got: " + String.Join(", ", types.Select(t => t.FullName).ToArray()));
throw new JsonSerializationException("Ambiguous type resolution, expected only one type but got: " + String.Join(", ", typesFound.Select(t => t.FullName).ToArray()));
}

return null;
}

internal virtual Dictionary<string, Type> GetTypesByPropertyPresence(Type parentType)
internal virtual List<TypeWithPropertyMatchingAttributes> GetTypesByPropertyPresence(Type parentType)
{
return GetAttributes<KnownSubTypeWithPropertyAttribute>(ToTypeInfo(parentType))
.ToDictionary(a => a.PropertyName, a => a.SubType);
.Select(a => new TypeWithPropertyMatchingAttributes(a.SubType, a.PropertyName, a.StopLookupOnMatch))
.ToList();
}

private Type GetTypeFromDiscriminatorValue(JObject jObject, Type parentType, JsonSerializer serializer)
Expand Down Expand Up @@ -407,14 +419,17 @@ private static Type GetTypeByName(string typeName, TypeInfo parentType)

return null;
}

static bool IsSubclassOfRawGeneric(TypeInfo generic, TypeInfo toCheck) {

static bool IsSubclassOfRawGeneric(TypeInfo generic, TypeInfo toCheck)
{
TypeInfo cur = toCheck;
TypeInfo objectTypeInfo = ToTypeInfo(typeof(object));
while (generic != cur && toCheck != objectTypeInfo) {
while (generic != cur && toCheck != objectTypeInfo)
{
cur = toCheck.IsGenericType ? ToTypeInfo(toCheck.GetGenericTypeDefinition()) : toCheck;
toCheck = ToTypeInfo(toCheck.BaseType);
}

return generic == cur;
}

Expand Down Expand Up @@ -478,7 +493,7 @@ private static IEnumerable<object> GetAttributes(TypeInfo typeInfo)
lock (_attributesCache)
{
if (_attributesCache.TryGetValue(typeInfo, out var res))
return res;
return res;

res = typeInfo.GetCustomAttributes(false);
_attributesCache.Add(typeInfo, res);
Expand Down
6 changes: 3 additions & 3 deletions JsonSubTypes/JsonSubtypesByPropertyPresenceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ namespace JsonSubTypes
{
internal class JsonSubtypesByPropertyPresenceConverter : JsonSubtypesConverter
{
private readonly Dictionary<string, Type> _jsonPropertyName2Type;
private readonly List<TypeWithPropertyMatchingAttributes> _jsonPropertyName2Type;

internal JsonSubtypesByPropertyPresenceConverter(Type baseType, Dictionary<string, Type> jsonProperty2Type, Type fallbackType) : base(baseType, fallbackType)
internal JsonSubtypesByPropertyPresenceConverter(Type baseType, List<TypeWithPropertyMatchingAttributes> jsonProperty2Type, Type fallbackType) : base(baseType, fallbackType)
{
_jsonPropertyName2Type = jsonProperty2Type;
}

internal override Dictionary<string, Type> GetTypesByPropertyPresence(Type parentType)
internal override List<TypeWithPropertyMatchingAttributes> GetTypesByPropertyPresence(Type parentType)
{
return _jsonPropertyName2Type;
}
Expand Down
14 changes: 10 additions & 4 deletions JsonSubTypes/JsonSubtypesWithPropertyConverterBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;

namespace JsonSubTypes
{
public class JsonSubtypesWithPropertyConverterBuilder
{
private readonly Type _baseType;
private readonly Dictionary<string, Type> _subTypeMapping = new Dictionary<string, Type>();
private readonly Dictionary<string, TypeWithPropertyMatchingAttributes> _subTypeMapping = new Dictionary<string, TypeWithPropertyMatchingAttributes>();
private Type _fallbackSubtype;

private JsonSubtypesWithPropertyConverterBuilder(Type baseType)
Expand All @@ -25,12 +26,17 @@ public static JsonSubtypesWithPropertyConverterBuilder Of<T>()
return Of(typeof(T));
}

public JsonSubtypesWithPropertyConverterBuilder RegisterSubtypeWithProperty(Type subtype, string jsonPropertyName)
public JsonSubtypesWithPropertyConverterBuilder RegisterSubtypeWithProperty(Type subtype, string jsonPropertyName, bool stopLookupOnMatch)
{
_subTypeMapping.Add(jsonPropertyName, subtype);
_subTypeMapping.Add(jsonPropertyName, new TypeWithPropertyMatchingAttributes(subtype, jsonPropertyName, stopLookupOnMatch));
return this;
}

public JsonSubtypesWithPropertyConverterBuilder RegisterSubtypeWithProperty(Type subtype, string jsonPropertyName)
{
return RegisterSubtypeWithProperty(subtype, jsonPropertyName, false);
}

public JsonSubtypesWithPropertyConverterBuilder RegisterSubtypeWithProperty<T>(string jsonPropertyName)
{
return RegisterSubtypeWithProperty(typeof(T), jsonPropertyName);
Expand All @@ -49,7 +55,7 @@ public JsonSubtypesWithPropertyConverterBuilder SetFallbackSubtype<T>()

public JsonConverter Build()
{
return new JsonSubtypesByPropertyPresenceConverter(_baseType, _subTypeMapping, _fallbackSubtype);
return new JsonSubtypesByPropertyPresenceConverter(_baseType, _subTypeMapping.Values.ToList(), _fallbackSubtype);
}
}
}
18 changes: 18 additions & 0 deletions JsonSubTypes/TypeWithPropertyMatchingAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;

namespace JsonSubTypes
{
internal class TypeWithPropertyMatchingAttributes
{
internal Type Type { get; }
internal string JsonPropertyName { get; }
internal bool StopLookupOnMatch { get; }

public TypeWithPropertyMatchingAttributes(Type type, string jsonPropertyName, bool stopLookupOnMatch)
{
Type = type;
JsonPropertyName = jsonPropertyName;
StopLookupOnMatch = stopLookupOnMatch;
}
}
}

0 comments on commit 87abcbf

Please sign in to comment.