diff --git a/AnyOf Solution.sln b/AnyOf Solution.sln index 0c1234a..ff997aa 100644 --- a/AnyOf Solution.sln +++ b/AnyOf Solution.sln @@ -46,6 +46,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppUsesClassLibrarie EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClassLibraryCommon", "examples\ClassLibraryCommon\ClassLibraryCommon.csproj", "{C7307358-326A-42D2-889C-61D1DAE285D2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnyOf.Newtonsoft.Json", "src\AnyOf.Newtonsoft.Json\AnyOf.Newtonsoft.Json.csproj", "{3BD0104F-3E63-4223-A4AD-372E68A6B0CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +98,10 @@ Global {C7307358-326A-42D2-889C-61D1DAE285D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {C7307358-326A-42D2-889C-61D1DAE285D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7307358-326A-42D2-889C-61D1DAE285D2}.Release|Any CPU.Build.0 = Release|Any CPU + {3BD0104F-3E63-4223-A4AD-372E68A6B0CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BD0104F-3E63-4223-A4AD-372E68A6B0CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BD0104F-3E63-4223-A4AD-372E68A6B0CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BD0104F-3E63-4223-A4AD-372E68A6B0CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -113,6 +119,7 @@ Global {6A1126B2-EFD3-4758-907C-31B010178D84} = {38805E90-A955-4097-97DC-537F7A0CD773} {8F07BB3B-6C03-4533-89D0-D4C6106CCB0C} = {38805E90-A955-4097-97DC-537F7A0CD773} {C7307358-326A-42D2-889C-61D1DAE285D2} = {38805E90-A955-4097-97DC-537F7A0CD773} + {3BD0104F-3E63-4223-A4AD-372E68A6B0CB} = {0B71158B-576B-4498-9D40-141D554D1272} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3586BB89-A981-4CD0-88DB-2DF513FE2B90} diff --git a/src/AnyOf.Newtonsoft.Json/AnyOf.Newtonsoft.Json.csproj b/src/AnyOf.Newtonsoft.Json/AnyOf.Newtonsoft.Json.csproj new file mode 100644 index 0000000..a444e27 --- /dev/null +++ b/src/AnyOf.Newtonsoft.Json/AnyOf.Newtonsoft.Json.csproj @@ -0,0 +1,19 @@ + + + + AnyOf.Newtonsoft.Json + net45;netstandard1.3;netstandard2.0;netstandard2.1 + {31D0104F-3E63-4223-A4AD-372E68A6B0CB} + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/AnyOf.Newtonsoft.Json/AnyOfJsonConverter.cs b/src/AnyOf.Newtonsoft.Json/AnyOfJsonConverter.cs new file mode 100644 index 0000000..3cd0653 --- /dev/null +++ b/src/AnyOf.Newtonsoft.Json/AnyOfJsonConverter.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace RestEaseClientGeneratorConsoleApp +{ + public class AnyOfJsonConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + if (serializer.NullValueHandling == NullValueHandling.Include) + { + serializer.Serialize(writer, value); + } + return; + } + + var currentValue = GetPropertyValue(value, "CurrentValue"); + serializer.Serialize(writer, currentValue); + } + + /// + /// See + /// - https://stackoverflow.com/questions/8030538/how-to-implement-custom-jsonconverter-in-json-net + /// - https://stackoverflow.com/a/59286262/255966 + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jObject = JObject.Load(reader); + + Type mostSuitableType = null; + int countOfMaxMatchingProperties = -1; + + // Take the names of elements from json data + var jObjectKeys = GetKeys(jObject); + + // Take the public properties of the parent class + var objectTypeProps = objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Select(p => p.Name).ToHashSet(); + + // Trying to find the right "KnownType" + foreach (var knownType in GetPropertyValue(existingValue, "Types")) + { + // Select properties of the inheritor, except properties from the parent class and properties with "ignore" attributes + var notIgnoreProps = knownType.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => !objectTypeProps.Contains(p.Name) && p.CustomAttributes.All(a => a.AttributeType != typeof(JsonIgnoreAttribute))); + + // Get serializable property names + var jsonNameFields = notIgnoreProps.Select(prop => + { + string jsonFieldName = null; + var jsonPropertyAttribute = prop.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(JsonPropertyAttribute)); + if (jsonPropertyAttribute != null) + { + // Take the name of the json element from the attribute constructor + int constructorArgumentsCount = jsonPropertyAttribute.ConstructorArguments.Count; + if (constructorArgumentsCount > 0) + { + var argument = jsonPropertyAttribute.ConstructorArguments.First(); + if (argument.ArgumentType == typeof(string) && !string.IsNullOrEmpty(argument.Value as string)) + { + jsonFieldName = (string)argument.Value; + } + } + } + + // Otherwise, take the name of the property + if (string.IsNullOrEmpty(jsonFieldName)) + { + jsonFieldName = prop.Name; + } + + return jsonFieldName; + }); + + var jKnownTypeKeys = new HashSet(jsonNameFields); + + // By intersecting the sets of names we determine the most suitable inheritor + int count = jObjectKeys.Intersect(jKnownTypeKeys).Count(); + + if (count == jKnownTypeKeys.Count) + { + mostSuitableType = knownType; + break; + } + + if (count > countOfMaxMatchingProperties) + { + countOfMaxMatchingProperties = count; + mostSuitableType = knownType; + } + } + + if (mostSuitableType != null) + { + object target = Activator.CreateInstance(mostSuitableType); + + using (JsonReader jObjectReader = CopyReaderForObject(reader, jObject)) + { + serializer.Populate(jObjectReader, target); + } + + return Activator.CreateInstance(objectType, target); + } + + throw new SerializationException($"Could not deserialize {objectType}, no suitable type found."); + } + + public override bool CanConvert(Type objectType) + { + return objectType.FullName.StartsWith("AnyOfTypes.AnyOf`"); + } + + private HashSet GetKeys(JObject obj) + { + return new HashSet(((IEnumerable>)obj).Select(k => k.Key)); + } + + private static JsonReader CopyReaderForObject(JsonReader reader, JObject jObject) + { + var jObjectReader = jObject.CreateReader(); + jObjectReader.CloseInput = reader.CloseInput; + jObjectReader.Culture = reader.Culture; + jObjectReader.DateFormatString = reader.DateFormatString; + jObjectReader.DateParseHandling = reader.DateParseHandling; + jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling; + jObjectReader.FloatParseHandling = reader.FloatParseHandling; + jObjectReader.MaxDepth = reader.MaxDepth; + jObjectReader.SupportMultipleContent = reader.SupportMultipleContent; + return jObjectReader; + } + + private static T GetPropertyValue(object value, string name) + { + return (T)GetPropertyValue(value, name); + } + + private static object GetPropertyValue(object value, string name) + { + return value.GetType().GetProperty(name).GetValue(value); + } + } +} \ No newline at end of file diff --git a/src/AnyOf.Newtonsoft.Json/Compatiblity/LinqExtensions.cs b/src/AnyOf.Newtonsoft.Json/Compatiblity/LinqExtensions.cs new file mode 100644 index 0000000..96cba4f --- /dev/null +++ b/src/AnyOf.Newtonsoft.Json/Compatiblity/LinqExtensions.cs @@ -0,0 +1,14 @@ +#if !NETSTANDARD2_1_OR_GREATER +using System.Collections.Generic; + +namespace System.Linq +{ + internal static class LinqExtensions + { + public static HashSet ToHashSet(this IEnumerable source, IEqualityComparer comparer = null) + { + return new HashSet(source, comparer); + } + } +} +#endif \ No newline at end of file