Sneaky Beaver Sneaky Beaver - 5 months ago 10
JSON Question

Serializing objects with nullable nested lists

I have the following objects that I want to serialize:

class Account
{
public Account(List<string> groups)
{
Groups = groups.Select(s=>new Group(s)).ToList();
}
[JsonProperty("Groups")]
public List<Group> Groups { get; set; }
}
class Group
{
public Group(string url)
{
Url = url;
}

public CommentSchedule NextCommentSchedule()
{
if (CommentSchedules.Count == 0) return null;
return CommentSchedules.Aggregate((curMin, x) => x.StartTime < curMin.StartTime ? x : curMin); // Result of inside function goes to curMin, next item in list goes to x
}

[JsonProperty("Url")]
public string Url { get; set; }

[JsonProperty("Comments")]
public List<Comment> Comments { get; set; }

[JsonProperty("CommentSchedules")]
public List<CommentSchedule> CommentSchedules { get; set; }
}
class Comment
{
public Comment(string text, int max)
{
Text = text;
MaxPosts = max;
}
[JsonProperty("Text")] // Text to post
public string Text { get; set; }

[JsonProperty("MaxPosts")] // Maximum number of times comment can be posted before being removed
public int MaxPosts { get; set; }
}


I have no problem deserializing it into the file I'm saving it in, and I can serialize the string when there are no groups, but when I try to serialize the json string after adding a group I get


Unexpected character encountered while parsing value: {. Path 'Groups', line 1, position 67.


I'm pretty sure this is because I'm saving it with empty lists inside of the groups so when it tries to serialize the empty nested lists it doesn't like that. How can I fix this?

dbc dbc
Answer

The problem is related to the fact that your Account class does not have a public parameterless constructor. Instead, it has a single parameterized constructor, like so:

class Account
{
    public Account(List<string> groups)
    {
        Groups = groups.Select(s=>new Group(s)).ToList();
    }
    [JsonProperty("Groups")]
    public List<Group> Groups { get; set; }
}

When Json.NET tries to deserialize such a class, it will construct a class instance by calling that constructor, matching the constructor arguments to the JSON properties by name using reflection and using default values for missing properties. Matching by name is case-insensitive, unless there are multiple matches that differ only in case, in which case the match becomes case sensitive.

Now, your constructor has a parameter named groups that with the same name (modulo case) as the property Groups. However, they are collections of objects of different types. The property returns a collection of classes that are mapped to Json.NET objects, whereas the constructor takes a collection of simple strings. This mismatch accounts for the serialization error your are seeing, as Json.NET cannot deserialize an object as a simple string.

The workaround is to provide either a parameterless constructor, or a parameterized constructor marked with [JsonConstructor] taking a List<Group> groups as an argument. Either constructor can be private as long as it is marked with [JsonConstructor]. I.e. either add:

    [JsonConstructor]
    Account()
    {
        Groups = new List<Group>();
    }

Or

    [JsonConstructor]
    Account(List<Group> groups)
    {
        Groups = groups;
    }

The fact that your Comment class only has a parameterized constructor is not a problem, since the data types of the arguments match the data types of its properties. You might want to change the name of the max argument to maxPosts so that the value is passed to the constructor rather than set later via the property:

    public Comment(string text, int maxPosts)
    {
        Text = text;
        MaxPosts = maxPosts;
    }

More generally, if you follow the convention that your constructor arguments should have the same name (modulo case) and type as the corresponding class members, then Json.NET should be able to deserialize your class.