Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fluent converters/mappers with Json.NET?

So, I got a bunch of classes I need to serialize/deserialize which also happen to be domain objects (at least some of 'em), thus I want them to be free of any attributes or not depending on a certain framework.

I looked at Custom Converters in Json.NET, but they look very 'ancient' to me in terms of usage and what not, since we nowadays have generics and it's not that hard to implement a fluent interface. So before I'm going down the road of weak typing etc..

...what I'm looking for (pseudo):

public class MyModel {
    public int Id { get; set; }
    public string Name { get; set; }
    public SomeObj SomeObj { get; set; }
}

public class MyModelConverter : JsonConverter<MyModel> {

    public JsonConverter() {
        RuleFor(x => x.Id).Name("Identifier");
        RuleFor(x => x.SomeObj).Name("Data")
            .WithConverter(new SomeObjConverter());
        RuleFor(x => x.Name).Ignore();
    }

}

Is there anything like that in Json.NET? Am I just missing something? (By the way I can't use different names for my properties etc. since the models are based on a 3rd party specification).

like image 381
xvdiff Avatar asked Nov 07 '14 12:11

xvdiff


2 Answers

Here is my take on achieving the desired API:

Edited as per comment

public abstract class Rule
{
    private Dictionary<string, object> rule { get; } = new Dictionary<string, object>();

    protected void AddRule(string key, object value)
    {
        if (rule.ContainsKey(key))
        {
            rule.Add(key, value);
        }
        else
        {
            rule[key] = value;
        }
    }

    protected IEnumerable<KeyValuePair<string, object>> RegisteredRules
    {
        get
        {
            return rule.AsEnumerable();
        }
    }
}

public abstract class PropertyRule : Rule
{
    public MemberInfo PropertyInfo { get; protected set; }

    public void Update(JsonProperty contract)
    {
        var props = typeof(JsonProperty).GetProperties();
        foreach (var rule in RegisteredRules)
        {
            var property = props.Where(x => x.Name == rule.Key).FirstOrDefault();
            if (property != null)
            {
                var value = rule.Value;
                if (property.PropertyType == value.GetType())
                {
                    property.SetValue(contract, value);
                }
            }
        }
    }
}

public class PropertyRule<TClass, TProp> : PropertyRule
{
    public const string CONVERTER_KEY = "Converter";
    public const string PROPERTY_NAME_KEY = "PropertyName";
    public const string IGNORED_KEY = "Ignored";

    public PropertyRule(Expression<Func<TClass, TProp>> prop)
    {
        PropertyInfo = (prop.Body as System.Linq.Expressions.MemberExpression).Member;
    }

    public PropertyRule<TClass, TProp> Converter(JsonConverter converter)
    {
        AddRule(CONVERTER_KEY, converter);
        return this;
    }

    public PropertyRule<TClass, TProp> Name(string propertyName)
    {
        AddRule(PROPERTY_NAME_KEY, propertyName);
        return this;
    }

    public PropertyRule<TClass, TProp> Ignore()
    {
        AddRule(IGNORED_KEY, true);
        return this;
    }
}

public interface SerializationSettings
{
    IEnumerable<Rule> Rules { get; }
}

public class SerializationSettings<T> : SerializationSettings
{
    private List<Rule> rules { get; } = new List<Rule>();

    public IEnumerable<Rule> Rules { get; private set; }

    public SerializationSettings()
    {
        Rules = rules.AsEnumerable();
    }

    public PropertyRule<T, TProp> RuleFor<TProp>(Expression<Func<T, TProp>> prop)
    {
        var rule = new PropertyRule<T, TProp>(prop);
        rules.Add(rule);
        return rule;
    }
}

public class FluentContractResolver : DefaultContractResolver
{
    static List<SerializationSettings> settings;

    public static void SearchAssemblies(params Assembly[] assemblies)
    {
        SearchAssemblies((IEnumerable<Assembly>)assemblies);
    }

    public static void SearchAssemblies(IEnumerable<Assembly> assemblies)
    {
        settings = assemblies.SelectMany(x => x.GetTypes()).Where(t => IsSubclassOfRawGeneric(t, typeof(SerializationSettings<>))).Select(t => (SerializationSettings)Activator.CreateInstance(t)).ToList();
    }

    //http://stackoverflow.com/questions/457676/check-if-a-class-is-derived-from-a-generic-class
    static bool IsSubclassOfRawGeneric(System.Type toCheck, System.Type generic)
    {
        if (toCheck != generic)
        {
            while (toCheck != null && toCheck != typeof(object))
            {
                var cur = toCheck.GetTypeInfo().IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
                if (generic == cur)
                {
                    return true;
                }
                toCheck = toCheck.GetTypeInfo().BaseType;
            }
        }
        return false;
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var contract = base.CreateProperty(member, memberSerialization);

        var rule = settings.Where(x => x.GetType().GetTypeInfo().BaseType.GenericTypeArguments[0] == member.DeclaringType).SelectMany(x => x.Rules.Select(r => r as PropertyRule).Where(r => r != null && r.PropertyInfo.Name == member.Name)).FirstOrDefault();
        if (rule != null)
        {
            rule.Update(contract);
        }
        return contract;
    }
}

Now in somewhere in program start-up:

FluentContractResolver.SearchAssemblies(typeof(MyModel).GetTypeInfo().Assembly);

Newtonsoft.Json.JsonConvert.DefaultSettings = () => 
{
    return new Newtonsoft.Json.JsonSerializerSettings
    {
        Formatting = Newtonsoft.Json.Formatting.Indented,
        ContractResolver = new FluentContractResolver()
    };
};

With this in place you now just need to add classes with fluent settings:

public class MyModelSettings : SerializationSettings<MyModel> 
{

    public MyModelSettings() 
    {
        RuleFor(x => x.Id).Name("Identifier");
        RuleFor(x => x.SomeObj).Name("Data").Converter(new SomeObjConverter());
        RuleFor(x => x.Name).Ignore();
    }

}
like image 68
Nripendra Avatar answered Nov 03 '22 22:11

Nripendra


Fluent-Json.NET allows you to map objects, use type discriminators, all without interfering with your data objects. No attributes are required.

Mapping classes

public class AnimalMap : JsonMap<Animal>
{
    public AnimalMap()
    {
        this.DiscriminateSubClassesOnField("class");
        this.Map(x => x.Speed, "speed");
    }
}

public class FelineMap : JsonSubclassMap<Feline>
{
    public FelineMap()
    {
        this.Map(x => x.SightRange, "sight");
    }
}

public class LionMap : JsonSubclassMap<Lion>
{
    public LionMap()
    {
        this.DiscriminatorValue("lion");
        this.Map(x => x.Strength, "strength");
    }
}

Model classes

public class Animal
{
    public Animal(float speed)
    {
        this.Speed = speed;
    }

    public float Speed { get; set; }
}

public abstract class Feline : Animal
{
    protected Feline(float speed, float sightRange) : base(speed)
    {
        this.SightRange = sightRange;
    }

    public float SightRange { get; set; }
}

public class Lion : Feline
{
    public Lion(float speed, float sightRange, float strength) : base(speed, sightRange)
    {
        this.Strength = strength;
    }

    public float Strength { get; set; }
}
like image 41
Akira Yamamoto Avatar answered Nov 03 '22 23:11

Akira Yamamoto