0

I have a class Person.cs which needs to store a list of Friends (List of Person.cs). However, it would be overkill to store the whole Person information in that array, just to have a complete object.

Json file

[ 
    { 
        "Username": "mary",    
        "DisplayName": "Mary Sanchez",
        "Age": 20,
        "Friends": [
            {"Username": "jonathan"},
            {"Username": "katy"}
        ]
    }, 
    { 
        "Username": "jonathan",
        "Age": 25,
        "DisplayName": "Jonathan White",    
        "Friends": [
            {"Username": "mary"},
            {"Username": "katy"}
        ]
    },
    { 
        "Username": "katy",   
        "DisplayName": "Katy Rivers", 
        "Age": 28,
        "Friends": [
            {"Username": "mary"},
            {"Username": "jonathan"}
        ]
    }
]

C# class

public class Person

{
        public string Username { get; set; }
        public string DisplayName { get; set; }
        public int Age { get; set; }
        public List<Person> Friends { get; set; }
}

As you can see the Property Friends is an array of Person. It would be overkill for me to redefine all the persons in the array of friends of the json file, especially if it's a large file. I can't instantiate 'mary', because her friends 'jonathan' and 'katy' appear later in the json file. As you see this becomes a nesting dependency problem. Also assume this file has a lot of circular references as everyone is friends with each other.

The problem is ofcourse that I will end up with incomplete Person object in the array of Friends. If I want to retrieve mary's friends ages, it will result in null values:

StreamReader reader = new StreamReader("Assets/Resources/Json/persons.json");
Persons = JsonConvert.DeserializeObject<List<Person>>(reader.ReadToEnd());
Person mary = Persons.Find(i => i.Username == "mary");
foreach(Person friend in mary.friends){
    int age = friend.Age; //this will be null
}

So I tried to fix it by looping through the list again and manually retrieving the friend variable:

foreach (Person friend in mary.Friends)
{
    Person friend = PersonManager.Persons.Find(i => i.Username == profile.Username);
    int age = friend.age;
}

This works, but is very inefficient though. Especially if I have more properties that are like the Friends property. Mind you, the Persons array is in a separate class and I had to make the list static in order to retrieve it all times (i believe this is bad practice too). Is there a better way to achieve what I'm trying to achieve in one go?

IT-Girl
  • 448
  • 5
  • 24
  • 1
    What code serializes the object-graph? If that code uses Newtonsoft too then use its built-in object-reference setting. – Dai Mar 07 '21 at 12:42
  • 2
    Map your person to some PersonVM or PersonDto and then deserialize. Your VM or DTO should contain only properties what you need – zolty13 Mar 07 '21 at 12:42
  • Guys thanks for your suggestions! I was missing double quotes somewhere, so now the json is ok again. And I researched DTO's , that is indeed what I need to implement! – IT-Girl Mar 07 '21 at 12:54
  • I deleted my answer because I found numerous errors in it, including one you pointed out. I’ll repost a better solution in the morning. – Dai Mar 07 '21 at 13:44
  • @Dai No problem, take your time! I did understand the basics and I'll wait patiently. – IT-Girl Mar 07 '21 at 13:46
  • 1
    @IT-Girl The correct solution (which does not support full PersonDtos in the Friends lists) would work like this: 1) deserialise to List. 2) Loop through and convert them all to Person instances without yet populating the Friends list. 3) Create the dictionary from the List instead of List. 4) In a second pass populate the `List[i].Friends` lists (using the dictionary for O(1) lookup) using the Friends list from the corresponding `List[i].Friends` list. – Dai Mar 07 '21 at 13:50
  • 2
    [`PreserveReferencesHandling.Objects`](https://www.newtonsoft.com/json/help/html/PreserveObjectReferences.htm#PreserveReferencesHandling) may do what you need, it adds an `"$id"` to an object the first time it is serialized, and serializes it with a `"$ref"` when subsequently encountered. See: [Serialize one to many relationships in Json.net](https://stackoverflow.com/q/5769200/3744182) and [JSON.NET Error Self referencing loop detected for type](https://stackoverflow.com/q/7397207/3744182). – dbc Mar 07 '21 at 14:14
  • And, to force all `Person` objects to be stored with references preserved, apply `[JsonObject(IsReference = true)]` to `Person`. Demo fiddle here: https://dotnetfiddle.net/aAKLgi. Note however that Json.NET always fully serializes the **first** occurrence of a given `Person` in the serialization graph, not the **topmost** occurrence. This is because it's a single-pass serializer. In your question, you fully serialize the topmost occurrences. If you want that you cannot use `PreserveReferencesHandling` and will need to preprocess and postprocess your data model instead. – dbc Mar 07 '21 at 14:28
  • 1
    The list of Person in Person is recursive and will probably cause bottomless serialization issue. A has B that has C that has A that has B.... you should store a list of person id and a dictionary of Person by id for fetching them. – Everts Mar 07 '21 at 16:19

1 Answers1

1

Btw, your JSON is invalid. You'll need to look at "Jonathan", see how the record has two UserName properties and no DisplayName. I assume the second occurrence should be named DisplayName.


The quick-and-easy solution is to use PreserveReferencesHandling, but this also needs to be used by the code that generates the JSON in the first place. If you have no control over the JSON being generated and you want to keep on using Json.NET (aka Newtonsoft.Json) then you'll need to define a separate DTO type (do not use class inheritance to avoid copying+pasting properties between your DTO type and your business/domain entity type, that's not what inheritance is for - if you're concerned about tedium then use T4 to generate partial types from a shared list of properties).

So add these classes:

public class PersonDto
{
    public string          UserName    { get; set; }
    public string          DisplayName { get; set; }
    public int             Age         { get; set; }
    public List<PersonRef> Friends     { get; set; }
}

public class PersonRef
{
    public string UserName { get; set; }
}

Then change your deserialization code to this:

public List<Person> GetPeople( String jsonText )
{
    List<PersonDto> peopleDto = JsonConvert.DeserializeObject< List<PersonDto> >( jsonText );

    List<Person> people = peopleDto
        .Select( p => ToPersonWithoutFriends( p ) )
        .ToList();

    Dictionary<String,PersonDto> peopleDtoByUserName = peopleDto.ToDictionary( pd => pd.UserName );

    Dictionary<String,Person> peopleByUserName = people.ToDictionary( p => p.UserName );

    foreach( Person p in people )
    {
        PersonDto pd = peopleDtoByUserName[ p.UserName ];
        
        p.Friends.AddRange(
            pd.Friends.Select( pr => peopleByUserName[ pr.UserName ] )
        );
    }

    return people;
}

private static Person ToPersonWithoutFriends( PersonDto dto )
{
    return new Person()
    {
         UserName    = dto.UserName,
         DisplayName = dto.DisplayName,
         Age         = dto.Age,
         Friends     = new List<Person>()
    };
}
Dai
  • 141,631
  • 28
  • 261
  • 374
  • Amazing, thanks! I am nearly there, however it's giving me "Cannot implicitly convert type List' to 'List", because Friends in Profile.cs is a List while peopleByUserName is PersonDto. Any idea what I can do to fix this? – IT-Girl Mar 07 '21 at 13:28
  • @IT-Girl I was wrong about being able to inline `ToPerson`. I’ve updated my answer. – Dai Mar 07 '21 at 13:42
  • 1
    @IT-Girl I've restored my answer now with the changes. – Dai Mar 08 '21 at 01:11