Last active
January 12, 2021 09:58
-
-
Save Pzixel/5a7fe56173371f7bb345bca2fd11edf2 to your computer and use it in GitHub Desktop.
Allows flattening JSON struct when (de)serializing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Linq.Expressions; | |
| using System.Reflection; | |
| using Newtonsoft.Json; | |
| using Newtonsoft.Json.Linq; | |
| using Newtonsoft.Json.Serialization; | |
| namespace ConsoleApp15 | |
| { | |
| [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] | |
| public class JsonFlattenAttribute : Attribute | |
| { | |
| } | |
| class JsonFlatteningConverter : JsonConverter | |
| { | |
| private static readonly JsonSerializer DefaultSerializer = new() | |
| { | |
| ContractResolver = new EraseFlatteningResolver() | |
| }; | |
| public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) | |
| { | |
| if (value is null) | |
| { | |
| writer.WriteNull(); | |
| return; | |
| } | |
| var token = JToken.FromObject(value, DefaultSerializer); | |
| var flattenedMemberInfo = GetFlattenedMemberInfo(value.GetType()); | |
| if (token[flattenedMemberInfo.Name] is JObject flattenedObject) | |
| { | |
| foreach (var property in flattenedObject.Properties()) | |
| { | |
| token[property.Name] = property.Value; | |
| } | |
| flattenedObject.Parent!.Remove(); | |
| } | |
| token.WriteTo(writer); | |
| } | |
| public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, | |
| JsonSerializer serializer) | |
| { | |
| var defaultSerializer = new JsonSerializer() {ContractResolver = new EraseFlatteningResolver()}; | |
| var token = JToken.ReadFrom(reader); | |
| var flattenedMember = GetFlattenedMemberInfo(objectType); | |
| var objectValue = token.ToObject(objectType, defaultSerializer); | |
| if (objectValue is null) | |
| { | |
| return null; | |
| } | |
| var flattenedValue = token.ToObject(flattenedMember.PropertyType, defaultSerializer); | |
| flattenedMember.PropertySetter(objectValue, flattenedValue); | |
| return objectValue; | |
| } | |
| public override bool CanConvert(Type objectType) => TryGetFlattenedMemberInfo(objectType) is { }; | |
| private static readonly | |
| Dictionary<Type, (string Name, Type PropertyType, Action<object, object> PropertySetter)?> | |
| FlattenedPropertyCache = new(); | |
| private static (string Name, Type PropertyType, Action<object, object> PropertySetter) | |
| GetFlattenedMemberInfo(Type objectType) => TryGetFlattenedMemberInfo(objectType)!.Value; | |
| private static (string Name, Type PropertyType, Action<object, object> PropertySetter)? | |
| TryGetFlattenedMemberInfo(Type objectType) | |
| { | |
| if (!FlattenedPropertyCache.ContainsKey(objectType)) | |
| { | |
| var flattenedMembers = objectType.GetMembers() | |
| .Where(p => p.GetCustomAttribute(typeof(JsonFlattenAttribute), true) is { }) | |
| .ToArray(); | |
| if (flattenedMembers.Length != 1) | |
| { | |
| FlattenedPropertyCache[objectType] = null; | |
| } | |
| else | |
| { | |
| var flattenedMember = flattenedMembers[0]; | |
| var declaringType = flattenedMember.DeclaringType!; | |
| var memberType = flattenedMember switch | |
| { | |
| PropertyInfo pi => pi.PropertyType, | |
| FieldInfo fi => fi.FieldType | |
| }; | |
| var obj = Expression.Parameter(typeof(object), "obj"); | |
| var valueToSet = Expression.Parameter(typeof(object), "valueToSet"); | |
| var setterExpression = Expression.Assign( | |
| Expression.MakeMemberAccess( | |
| Expression.Convert(obj, declaringType), flattenedMember), | |
| Expression.Convert(valueToSet, memberType)); | |
| var lambda = Expression.Lambda<Action<object, object>>(setterExpression, obj, valueToSet); | |
| FlattenedPropertyCache[objectType] = (flattenedMember.Name, memberType, lambda.Compile()); | |
| } | |
| } | |
| return FlattenedPropertyCache[objectType]; | |
| } | |
| } | |
| internal sealed class EraseFlatteningResolver : DefaultContractResolver | |
| { | |
| protected override JsonObjectContract CreateObjectContract(Type objectType) | |
| { | |
| var jsonObjectContract = base.CreateObjectContract(objectType); | |
| if (jsonObjectContract.Converter is JsonFlatteningConverter) | |
| { | |
| jsonObjectContract.Converter = null; | |
| } | |
| return jsonObjectContract; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using Newtonsoft.Json; | |
| namespace ConsoleApp15 | |
| { | |
| class Program | |
| { | |
| static void Main(string[] args) | |
| { | |
| var value = new Bar | |
| {Z = "Hello", Foo = new Foo() {X = 10, Y = Guid.Empty}}; | |
| Console.WriteLine($"Original: Z = {value.Z}, Foo = {{ X = {value.Foo.X}, Y = {value.Foo.Y} }}"); | |
| var serialized = JsonConvert.SerializeObject(value); | |
| Console.WriteLine(serialized); | |
| var deserialized = JsonConvert.DeserializeObject<Bar>(serialized); | |
| Console.WriteLine( | |
| $"Deserialized: Z = {deserialized.Z}, Foo = {{ X = {deserialized.Foo.X}, Y = {deserialized.Foo.Y} }}"); | |
| } | |
| } | |
| class Foo | |
| { | |
| public int X { get; set; } | |
| public Guid Y { get; set; } | |
| } | |
| [JsonConverter(typeof(JsonFlatteningConverter))] | |
| class Bar | |
| { | |
| public string Z { get; set; } | |
| [JsonFlatten] | |
| public Foo Foo { get; set; } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment