biscuit314
biscuit314

Reputation: 2414

How to group properties into a sub-object when serializing to JSON

Given this class:

public class Thing
{
    public string Alpha { get; set; }
    public string Beta { get; set; }            
}

I need to serialize arbitrary subclasses of Thing which subclasses may themselves add Thing properties. For example...

public class SomeThing : Thing
{
  public string Delta {get; set; }

  public Thing ThisThing { get; set; }
  public Thing ThatThing { get; set; }
}

It is easy, using Newtonsoft Json.NET to serialize a SomeThing class to this:

{
  alpha: "x",
  beta: "x",
  delta: "x",

  thisThing: {
    alpha: "y",
    beta: "y"
  },
  thatThing: {
    alpha: "z",
    beta: "z"
  }
}

What I want to do, though, is this (without changing the Thing or SomeThing classes):

{
  alpha: "x",
  beta: "x",
  delta: "x",

  things: {
    thisThing: {
      alpha: "y",
      beta: "y"
    },
    thatThing: {
      alpha: "z",
      beta: "z"
  }  
}

That is, I want to collect any Thing properties into a sub-object named things.

Another example:

public class SomeThingElse : Thing
{
  public int Gamma {get; set; }

  public Thing Epsilon { get; set; }
}

...would serialize to

{
  alpha: "x",
  beta: "x",
  gamma: 42,

  things: {
    epsilon: {
      alpha: "y",
      beta: "y"
    }
  }
}

By creating a contract resolver I can easily peel out the individual thing properties and leave the non-things to serialize themselves. But I don't know how to create the things property and stuff back in the properties I peeled out:

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

        // grab the properties that are NOT a Thing
        var toCreate = properties.Where(p => !typeof(Thing).IsAssignableFrom(p.PropertyType)).ToList();

        // grab the properties that ARE a Thing            
        var toGroup = properties.Where(p => typeof(Thing).IsAssignableFrom(p.PropertyType)).ToList();

        // create the new things property to stuff toGroup into
        var things = new JsonProperty
        {
            PropertyName = "things"
        };

        // THIS IS WHERE I'M STUCK...
        // TODO: somehow stuff toGroup into "things"

        // put the group back along with the non-thing properties
        toCreate.Add(things);

        // return the re-combined set of properties
        return toCreate;
    }            
}

I use this resolver as follows (simplified for this question):

static void Main(string[] args)
{
    var st = new SomeThing
    {
        Alpha = "x",
        Beta = "x",
        Delta = "x",
        ThisThing = new Thing() {Alpha = "y", Beta = "y"},
        ThatThing = new Thing() {Alpha = "z", Beta = "z"}
    };

    var settings = new JsonSerializerSettings
    {
        ContractResolver = new MyContractResolver(),
        Formatting = Formatting.Indented
    };

    var result = JsonConvert.SerializeObject(st, settings);

    Console.WriteLine(result);
}

Which produces

{
  alpha: "x",
  beta: "x",
  delta: "x"
}

Notice that even though I've created and added a JsonProperty with the name of "things" it does not appear. My hope is I just need to fill in the blanks near the TODO in the contract resolver.

Or maybe I'm going in the wrong direction. Can you help me?

Upvotes: 2

Views: 2398

Answers (4)

Brian Rogers
Brian Rogers

Reputation: 129707

It is possible to do what you want using a custom IContractResolver in combination with a custom IValueProvider. Try this:

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

        // if the type is a Thing and has child properties that are things...
        if (typeof(Thing).IsAssignableFrom(type) &&
            properties.Any(p => typeof(Thing).IsAssignableFrom(p.PropertyType)))
        {
            // grab only the properties that are NOT a Thing
            properties = properties
                .Where(p => !typeof(Thing).IsAssignableFrom(p.PropertyType))
                .ToList();

            // Create a virtual "things" property to group the remaining properties
            // into; associate the new property with a ValueProvider that will do
            // the actual grouping when the containing object is serialized
            properties.Add(new JsonProperty
            {
                DeclaringType = type,
                PropertyType = typeof(Dictionary<string, object>),
                PropertyName = "things",
                ValueProvider = new ThingValueProvider(),
                Readable = true,
                Writable = false
            });
        }

        return properties;
    }

    private class ThingValueProvider : IValueProvider
    {
        public object GetValue(object target)
        {
            // target should be a Thing; we want to get its Thing properties
            // and group them into a Dictionary.
            return target.GetType().GetProperties()
                         .Where(p => typeof(Thing).IsAssignableFrom(p.PropertyType))
                         .ToDictionary(p => p.Name, p => p.GetValue(target));
        }

        public void SetValue(object target, object value)
        {
            throw new NotImplementedException();
        }
    }
}

Demo:

class Program
{
    static void Main(string[] args)
    {
        SomeThing st = new SomeThing
        {
            Alpha = "x.a",
            Beta = "x.b",
            ThisThing = new Thing { Alpha = "y.a", Beta = "y.b" },
            ThatThing = new SomeThingElse 
            { 
                Alpha = "z.a", 
                Beta = "z.b",
                Delta = 42,
                Epsilon = new Thing { Alpha = "e.a", Beta = "e.b" }
            }
        };

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ContractResolver = new MyContractResolver();
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(st, settings);

        Console.WriteLine(json);
    }
}

public class Thing
{
    public string Alpha { get; set; }
    public string Beta { get; set; }
}

public class SomeThing : Thing
{
    public Thing ThisThing { get; set; }
    public Thing ThatThing { get; set; }
}

public class SomeThingElse : Thing
{
    public int Delta { get; set; }
    public Thing Epsilon { get; set; }
}

Output:

{
  "alpha": "x.a",
  "beta": "x.b",
  "things": {
    "thisThing": {
      "alpha": "y.a",
      "beta": "y.b"
    },
    "thatThing": {
      "delta": 42,
      "alpha": "z.a",
      "beta": "z.b",
      "things": {
        "epsilon": {
          "alpha": "e.a",
          "beta": "e.b"
        }
      }
    }
  }
}

Upvotes: 2

Andrew Whitaker
Andrew Whitaker

Reputation: 126052

Here's another converter that uses reflection to get the Things:

public class MyConverter : JsonConverter
{
    public override bool CanRead { get { return false; } }

    public override object ReadJson(
        JsonReader reader,
        Type objectType,
        object existingValue,
        JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(
        JsonWriter writer,
        object value,
        JsonSerializer serializer)
    {
        var someThing = (SomeThing)value;

        var things = typeof(SomeThing).GetProperties()
            .Where(pr => pr.PropertyType.IsAssignableFrom(typeof(Thing)))
            .ToDictionary (pr => pr.Name, pr => pr.GetValue(someThing));

        var nonThings = typeof(SomeThing).GetProperties()
            .Where(pr => !pr.PropertyType.IsAssignableFrom(typeof(Thing)));

        writer.WriteStartObject();
        writer.WritePropertyName("things");
        serializer.Serialize(writer, things);

        foreach (var nonThing in nonThings)
        {   
            writer.WritePropertyName(nonThing.Name);
            serializer.Serialize(writer, nonThing.GetValue(someThing));
        }

        writer.WriteEndObject();
    }

    public override bool CanConvert(Type type)
    {
        return type == typeof(SomeThing);
    }
}

I am having trouble figuring out how to respect the CamelCasePropertyNamesContractResolver and use the converter though.

Upvotes: 1

gravidThoughts
gravidThoughts

Reputation: 633

I think you may be going in the wrong direction.

If contained Thing objects are zero to many you could define your property

List<Thing> Things;

I believe at this point json.Net would serialize how you want without a Contract resolver.

Upvotes: 0

Related Questions