Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to flatten a referenced object into two json.net properties on the referer?

Consider the following class:

public class User
{
  public virtual int Id {get;set;}
  public virtual string Name {get;set;}
  public virtual User Superior {get;set;}
}

My goal is to serialize this as json using newtonsofts json.net like so:

{
  Id: 101,
  Name: 'Mithon',
  SuperiorId: 100,
  SuperiorName: 'TheMan'
}

Why do I want to do this? Because I want to use the Json as my DTO's without generating an intermediate layer of dynamic objects. Generating the DTO's should be done dynamically by convention rather than explicitly, imho. I know some might strongly disagree with this, but discussing my approach is besides the point. I just want to know if and how it can be done.

The challenge is that using JsonPropertyAttribute for the Superior property will yield only one property as output, where I need two. If I use a JsonObjectAttribute I will get a nested attribute and I would have trouble with the top level User also being flattened.

Luckily it seems there are enough protected and/or public properties and methods in the json.net library that I can extend something to get the desired result. The question then is which classes and methods should I start with to get where I want to go? Would deriving from DefaultContractResolver, and overriding the GetProperties method be good places, or should I look elsewhere?

like image 788
Gaute Løken Avatar asked Mar 11 '13 08:03

Gaute Løken


1 Answers

The short answer is yes, that would be appropriate places to start. Here's what I ended up with (for now):

public class MyContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(type, memberSerialization);

        foreach (var pi in type.GetProperties().Where(pi => typeof (Entity).IsAssignableFrom(pi.DeclaringType) && Attribute.IsDefined((MemberInfo) pi, typeof(IdNameMemberAttribute))))
        {
            properties.Add(CreateReferenceProperty(pi, Reflect.GetProperty<Entity>(e => e.Id)));
            properties.Add(CreateReferenceProperty(pi, Reflect.GetProperty<Entity>(e => e.Name)));
        }

        return properties;
    }

    private JsonProperty CreateReferenceProperty(PropertyInfo reference, PropertyInfo referenceMember)
    {
        var jsonProperty = base.CreateProperty(reference, MemberSerialization.OptOut);
        jsonProperty.PropertyName += referenceMember.Name;
        jsonProperty.ValueProvider = new ReferencedValueProvider(reference, referenceMember);
        jsonProperty.Writable = false;

        return jsonProperty;
    }
}

IdNameMemberAttribute is just an empty attribute that I use to annotate which reference properties I want to serialize. The important bit is that I do NOT annotate it with anything that Json.NET will recognize and use to generate a JsonProperty. That way I don't end up with a duplicate JsonProperty from my CreateProperties.

Alternatively I could have derived from DataMemberAttribute and looked for, modified and cloned the JsonProperty to represent my Id and Name.

For my asp.net web api I then set this MyContractResolver as the ContractResolver of JsonFormatter.SerializerSettings.

So that fixed me up for serialization. For deserialization I have a custom ChangeSet object where I store PropertyInfo and objects. During deserialization I then make sure I keep the Ids, and later resolve those from my data store, in my case using custom ActionFilter with access to the data store session.

Here is the essence of my serialization:

var jsonSource = streamReader.ReadToEnd();
var deserializedObject = JsonConvert.DeserializeObject(jsonSource, type, SerializerSettings);
var changeSet = deserializedObject as PropertyChangeSet;
if (changeSet != null)
{
    var jsonChange = JObject.Parse(jsonSource)["Change"].Cast<JProperty>().ToArray();

    IDictionary<string, int> references = jsonChange.Where(IsReferenceId).ToDictionary(t => t.Name.Substring(0, t.Name.Length - 2), t => t.Value.ToObject<int>());
    changeSet.References = references;

    var properties = jsonChange.Where(jp => !IsReferenceId(jp)).Select(t => t.Name).ToList();
    changeSet.Primitives = properties;
}

And presto, all the gory details of my clean entities and dynamic serialization are encapsulated, sadly in two places, but it couldn't be helped since I don't want to access my data source from the serializer.

like image 106
Gaute Løken Avatar answered Dec 09 '22 15:12

Gaute Løken