Is there any way to JSON.NET-serialize a subclass of List<T> that also has extra properties?
Asked Answered
R

3

12

Ok, we're using Newtonsoft's JSON.NET product, which I really love. However, I have a simple class structure for hierarchical locations that look roughly like this...


public class Location {
    public string Name { get; set; }
    public LocationList Locations { get; set; }
}

// Note: LocationList is simply a subclass of a List<T>
// which then adds an IsExpanded property for use by the UI.
public class LocationList : List<Location> {
    public bool IsExpanded { get; set; }
}

public class RootViewModel {
    public LocationList RootLocations{ get; set; }
}

...and when I serialize them to JSON, it all works great, except the IsExpanded property on the LocationList class is excluded. Only the list's contents are serialized.

Now here's what I'm envisioning would be a good format. It's esentially the same thing as if LocationList wasn't a subclass of List<Location> but rather was just a regular object that had a property called Items of type List<Location> instead.

{
  "Locations" : {
    "IsExpanded" : true,
    "Items" : [
      {
        "Name" : "Main Residence",
        "Locations" : {
          "IsExpanded" : true,
          "Items" : [
            {
              "Name" : "First Floor",
              "Locations" : {
                "IsExpanded" : false,
                "Items" : [
                  {
                    "Name" : "Livingroom"
                  },
                  {
                    "Name" : "Dining Room"
                  },
                  {
                    "Name" : "Kitchen"
                  }
                ]
              }
            },
            {
              "Name" : "Second Floor",
              "Locations" : {
                "IsExpanded" : false,
                "Items" : [
                  {
                    "Name" : "Master Bedroom"
                  },
                  {
                    "Name" : "Guest Bedroom"
                  }
                ]
              }
            },
            {
              "Name" : "Basement"
            }
          ]
        }
      }
    ]
  }
}

Now I also understand that Newtonsoft's product is extensible because they specifically talk about how you can write your own custom serializer for specific data types, which would be exactly what I'd want here. However, they don't have any good code examples on how to do this.

If we (the SO community) can figure this out, technically by using the above format we should be able to serialize ANY subclass of List (or its derivatives/similar objects) provided they don't already have a property called Items (which IMHO would be a poor design in the first place since it would be confusing as crap!) Perhaps we can even get Newtonsoft to roll such a thing in their serializer natively!

So that said... anyone know how to customize the serializer/deserializer to treat this object differently?

M

Robbie answered 2/5, 2011 at 23:15 Comment(8)
You serialize it to json with what? json.net, datacontracts...?Beforehand
Does it make a difference if you use the automatic property notation for the IsExpanded property? In other words, public bool IsExpanded { get; set; }? Not sure if that has anything to do with it, but it's the thing that's standing out to me as being different about that property.Accident
@Accident I can't see that as being the issue since the compiled IL would be identical either way. The only difference would be the name of the private field. I think it more likely that whatever serializer being used optimizes for List, and is not properly picking up that LocationList is a derived type.Kudu
@Harry Steinhilber: Yes, I agree. Good point.Accident
@ataddeini, I changed it back. Was bugging me why I had it that way here, but then I remembered in the code I copied, we were using the member variable but I had stripped that part out for clarity. I just forgot to change it to the shorter version here, which as I said, now I have. Thanks! :)Robbie
@Stanislav Ageev, we're using Newtonsoft's JSON.NET product. Love it (except for this issue!)Robbie
This behavior makes no sense, and now it's doing the opposite, serializing the properties of the list subclass, but none of its items.Paniagua
Here is a similar question with an alternative solution.Curare
A
2

Usually when I find myself fighting something like this it tells me I should consider another approach. In this case, I would recommend the following view model structure as an alternative:

public class Location
{
    public bool IsExpanded { get; set; }
    public string Name { get; set; }
    public List<Location> Locations { get; set; }
}

public class ViewModel
{
    public List<Location> RootLocations { get; set; }
}
Accident answered 3/5, 2011 at 1:12 Comment(8)
I Thought about that but IMHO that way is bad for two reasons. One, that separates the property from what it applies to, namely the list (and thus you have to duplicate it again on the root ViewModel object as well as anywhere else you want such a list. This also muddies up your bindings and such since it is a different object. But more importantly, two, you should not have to change the (view)model because of a limitation in the serializer! That's backwards! You find a newer storage mechanism if the one you have doesn't fulfill your needs.Robbie
@MarqueIV: Reasonable tradeoffs in my opinion. Better to address the issue and move on instead of dwelling on it and taking away from other tasks.Accident
touche! Well half-touche as you've now added the aforementioned binding and such issues that weren't there before. So purist: I win. Making money instead of gloating you're a purist: You do. (...and only one of us likely has 'Always meets deadlines' on our resumes! LOL!)Robbie
You should do it this way. How is someone going to deserialize you correctly if someone else gets a hold of whats serialized. Name the child list "Items" and your naming problem goes away.Ouphe
And when you don't own the type that you want to serialize? It kind of bites (and is surprising) that JSON.NET falls flat in this area.Wholehearted
@Brad, not sure what you mean about a 'naming problem'. The issue is a property that's being ignored by the serializer because it sees it's of a list type so it doesn't look at anything else except its children. Very frustrating.Robbie
I haven't looked at this in forever. But if I recall correctly, most frameworks will automatically support it if the child list is named "Items". I think JSON.NET does this, and so does a few others I checked at the time. This is 3 year old speculation.Ouphe
I'm seeing the exact opposite problem with JSON.NET. My subclass of LIst is having all it's properties serialized correctly, except the list items. It's not serializing any of the list items at all. It seems to be treating it as an object and completely ignoring the fact that it's a list. There must be some special logic for handling generic lists.Paniagua
R
1

Ok... so here's what I've come up with. I had to write my own JsonConverter. I basically use it to create an inline JObject that has the properties structured as I wanted them to persist, then I persist that. I then do the reverse when I read it back out.

However, the down'side is it doesn't use reflection or any other such things so this only works for this specific type which I had to hand-code property by property (in this case there are only two so that's good!) and it also doesn't take advantage of the DefaultValues processing which I have to re-emulate manually, meaning the attributes are basically ignored unless I reflect upon them. Still, this works. Perfect? No, but hey... things rarely are!

Of course, comments are welcome and encouraged!

public class LocationListJsonConverter : JsonConverter
{
    public override bool CanConvert(System.Type objectType)
    {
        return objectType == typeof(LocationList);
    }

    public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer)
    {
        var locationList = (existingValue as LocationList) ?? new LocationList();
        var jLocationList = JObject.ReadFrom(reader);

        locationList.IsExpanded = (bool)(jLocationList["IsExpanded"] ?? false);

        var jLocations = jLocationList["_Items"];
        if(jLocations != null)
        {
            foreach(var jLocation in jLocations)
            {
                var location = serializer.Deserialize<Location>(new JTokenReader(jLocation));
                locationList.Add(location);
            }
        }

        return locationList;

    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var locationList = value as LocationList;

        JObject jLocationList = new JObject();

        if(locationList.IsExpanded)
            jLocationList.Add("IsExpanded", true);

        if(locationList.Count > 0)
        {
            var jLocations = new JArray();

            foreach(var location in locationList)
            {
                jLocations.Add(JObject.FromObject(location, serializer));
            }

            jLocationList.Add("_Items", jLocations);

        }

        jLocationList.WriteTo(writer);

    }

}
Robbie answered 4/5, 2011 at 20:18 Comment(2)
I have also come accross the same thing. Did you find any better solution all this while?Pernell
Nope. Still looking. Lemme know if you too find something.Robbie
T
0

I need a class named FieldGroup that also has some properties to group some Fields. I did that as this firstly.

public class FieldGroup : List<Field>{ ... }

It has the problem to serialize as the post said. So I modified the class as below. So I can handle with it the same as the class of *FieldGroup that derived from List<Field>.

public class FieldGroup : IPrintable, IEnumerable<Field>
{
    public PrintFormat GroupFormat { get; set; } = new PrintFormat();
    public List<Field> Fields { get; set; } = new List<Field>();

    public Field this[int index]
    {
        get => Fields[index];
        set => Fields[index] = value;
    }

    public void Add(Field field)
    {
        Fields.Add(field);
    }
    public IEnumerator<Field> GetEnumerator()
    {
        return new FieldEnumerator(Fields);
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    ...
}
Tedra answered 12/9, 2020 at 1:49 Comment(2)
Hi, and welcome to Stack!:) Question for you... what is FieldEnumerator here? Was that a class you created to enumerate your fields? If so, couldn't you just have done this... return Fields.GetEnumerator(); because Fields is a List which can already be enumerated, so why not just delegate to it? Also, technically this doesn't answer my question which is adding properties to an existing collection. This wraps an existing collection, but then lets you enumerate over it as if it were that collection. Clever, but not quite what I was asking. Still, it's good info for others.Robbie
@MarkA.Donohoe:Yes! You are pretty right. Thank you for your advise! The way is just a trick for me to avoid changing code elsewhere. Hope to offer little help for others:).Tedra

© 2022 - 2024 — McMap. All rights reserved.