Skip to content

Instantly share code, notes, and snippets.

@Pzixel
Last active January 12, 2021 09:58
Show Gist options
  • Select an option

  • Save Pzixel/5a7fe56173371f7bb345bca2fd11edf2 to your computer and use it in GitHub Desktop.

Select an option

Save Pzixel/5a7fe56173371f7bb345bca2fd11edf2 to your computer and use it in GitHub Desktop.
Allows flattening JSON struct when (de)serializing
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;
}
}
}
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