CodeGym /Cursos /C# SELF /Trabalhando com objetos aninhados e coleções

Trabalhando com objetos aninhados e coleções

C# SELF
Nível 45 , Lição 1
Disponível

1. Introdução

Serializar objetos simples é como enviar um cartão pelo correio: é simples, nada se perde. Mas muitas vezes nossos objetos viram "álbuns de família": têm objetos aninhados, coleções e até coleções de coleções.

Imagine que nosso usuário não é só um Person com nome e idade, por exemplo ele pode ter um conjunto de contatos (Contact), vários endereços (casa/trabalho), e talvez um campo do tipo List<Pet>, se a pessoa gosta de animais. Tudo isso são objetos aninhados e coleções.

Como os serializers do .NET lidam com isso? Quais detalhes devemos considerar ao serializar árvores de objetos complexas? Pode quebrar tudo se houver outras coleções dentro de coleções? Hoje vamos entender o que esperar — e o que fazer nos casos complicados.

O que significa "aninhamento" em termos de .NET

Um objeto aninhado é simplesmente outro objeto como propriedade ou campo dentro do seu objeto principal. Por exemplo, nosso modelo de usuário estendido:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Contact> Contacts { get; set; }         // Coleção de objetos aninhados
    public Address? HomeAddress { get; set; }           // Um objeto aninhado (nullable)
}
public class Contact
{
    public string Type { get; set; }                    // Por exemplo, Email ou Phone
    public string Value { get; set; }
}
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}

Repare — a pessoa pode ter vários contatos, e o endereço é só um (e pode ser null). Isso é clássico em muitos modelos.

2. Serializando objetos e coleções com System.Text.Json

Exemplo simples: serializar e desserializar uma coleção

Vamos ver como isso funciona na prática:

using System.Text.Json;

var person = new Person
{
    Name = "Anna",
    Age = 28,
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "anna@example.com" },
        new Contact { Type = "Phone", Value = "+1234567890" }
    },
    HomeAddress = new Address { City = "Berlim", Street = "Alexanderplatz, 1" }
};

string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

O resultado será algo assim:

{
  "Name": "Anna",
  "Age": 28,
  "Contacts": [
    {
      "Type": "Email",
      "Value": "anna@example.com"
    },
    {
      "Type": "Phone",
      "Value": "+1234567890"
    }
  ],
  "HomeAddress": {
    "City": "Berlim",
    "Street": "Alexanderplatz, 1"
  }
}

O serializer entende bem o aninhamento. Se a propriedade é outro objeto, ele o serializa "aninhado". Se é uma lista, serializa como um array.

Fato: Essa serialização funciona automaticamente para qualquer coleção e objetos aninhados, se eles forem públicos e tiverem getter e setter públicos (get/set).

Desserialização: funciona "out of the box"

string jsonInput = /* JSON que acabamos de receber */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Anna"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"

Tudo funciona — os contatos viram de volta um List<Contact>, HomeAddress volta a ser um objeto da classe Address.

3. Como são serializadas as coleções: List, arrays, Dictionary

Frequentemente no seu modelo há não só listas (List<T>), mas dicionários (Dictionary<TKey, TValue>) ou arrays multidimensionais.

Exemplo com array

public class Team
{
    public string Name { get; set; }
    public Person[] Members { get; set; }
}

var team = new Team
{
    Name = "Desenvolvedores",
    Members = new[]
    {
        new Person { Name = "Aleksei", Age = 31 },
        new Person { Name = "Ekaterina", Age = 27 }
    }
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Name": "Desenvolvedores",
  "Members": [
    {
      "Name": "Aleksei",
      "Age": 31,
      "Contacts": null,
      "HomeAddress": null
    },
    {
      "Name": "Ekaterina",
      "Age": 27,
      "Contacts": null,
      "HomeAddress": null
    }
  ]
}

Serializar uma coleção é parecido com empacotar um monte de correspondências iguais: cada elemento é um cartão separado.

Exemplo com Dictionary

public class Phonebook
{
    public Dictionary<string, string> Phones { get; set; }
}

var phonebook = new Phonebook
{
    Phones = new Dictionary<string, string>
    {
        { "Andrei", "+12998887766" },
        { "Maria", "+12882223344" }
    }
};

string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Phones": {
    "Andrei": "+12998887766",
    "Maria": "+12882223344"
  }
}

Em JSON, o dicionário vira um objeto com chaves dinâmicas.

4. Coleções aninhadas (List dentro de List, arrays de arrays)

.NET sabe serializar essas "matrioshkas" também:

public class Zoo
{
    public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }

var zoo = new Zoo
{
    AnimalGroups = new List<List<Animal>>
    {
        new List<Animal> { new Animal { Name = "Leão" }, new Animal { Name = "Tigre" } },
        new List<Animal> { new Animal { Name = "Urso" } }
    }
};

string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "AnimalGroups": [
    [ { "Name": "Leão" }, { "Name": "Tigre" } ],
    [ { "Name": "Urso" } ]
  ]
}

O mesmo vale para desserialização.

5. Particularidades da serialização de objetos aninhados com XmlSerializer

Exemplo de serialização de objeto aninhado

using System.Xml.Serialization;
using System.IO;

var person = new Person
{
    Name = "Ivan",
    Age = 35,
    HomeAddress = new Address { City = "Bonn", Street = "Beethoven str., 100" },
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "ivan@domain.de" }
    }
};

var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());

Resultado:

<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Ivan</Name>
  <Age>35</Age>
  <Contacts>
    <Contact>
      <Type>Email</Type>
      <Value>ivan@domain.de</Value>
    </Contact>
  </Contacts>
  <HomeAddress>
    <City>Bonn</City>
    <Street>Beethoven str., 100</Street>
  </HomeAddress>
</Person>

Características das coleções no XmlSerializer

Por padrão o XmlSerializer serializa a coleção como a tag "Contacts", dentro da qual para cada elemento é criado um tag "Contact". Para isso, o tipo da coleção precisa ser público e os elementos precisam ser tipos serializáveis.

Ponto importante. Se a coleção estiver vazia, o XML ainda inclui uma tag vazia:

<Contacts />

Coleções suportadas. O XmlSerializer suporta List<T>, arrays T[], coleções que implementam ICollection<T>. Não suporta, por exemplo, Dictionary<TKey, TValue>! Para serializar dicionários será preciso usar técnicas adicionais ou bibliotecas externas.

6. Suporte a objetos e coleções no Newtonsoft.Json

O Newtonsoft.Json tem um suporte a aninhamento um pouco mais flexível. Em 99% dos casos tudo funciona igual ao System.Text.Json, mas tem vantagens: dá para serializar até campos privados (se explicitamente configurado), dicionários com chaves mais complexas, tipos dinâmicos, e até referências cíclicas (com configurações específicas).

Exemplo

using Newtonsoft.Json;

var person = new Person
{
    Name = "Pavel",
    Age = 40,
    Contacts = new List<Contact>
    {
        new Contact { Type = "SMS", Value = "+10000000013" }
    }
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);

Diferença. Se um objeto tem, por exemplo, campos privados, dá para serializá-los configurando um ContractResolver. Mas para coleções e objetos aninhados básicos tudo funciona automaticamente "out of the box".

7. Erros típicos, armadilhas e nuances

Erro 1: Objetos aninhados são null.
Se no objeto que você serializa algumas propriedades não estiverem preenchidas (null), no JSON ou XML elas podem não aparecer, ou podem ser gravadas como null. Por exemplo, se HomeAddress de Person for null, no JSON:

"HomeAddress": null

No XML por padrão essa tag não existirá — dá para incluir usando configurações do serializer ([XmlElement(IsNullable=true)]).

Erro 2: Serializar propriedades/campos privados.
Por padrão a maioria dos serializers (JSON e XML) trabalham só com propriedades públicas. Se quiser serializar campos privados/protected, terá que torná-los públicos ou configurar o serializer (por exemplo via ContractResolver no Newtonsoft.Json).

Erro 3: Classes sem construtor padrão.
Na serialização XML (e muitas vezes no JSON) a classe precisa ter um construtor público sem parâmetros. Se não houver, ocorrerá exceção durante a desserialização.

Erro 4: Serializar dicionários em XML.
O XmlSerializer não suporta serialização direta de Dictionary<TKey, TValue>. Isso costuma surpreender. Solução: envolver o dicionário numa lista de pares ou usar outro serializer.

Erro 5: Referências cíclicas.
Se um objeto contém por referência outro objeto que por sua vez referencia o primeiro (por exemplo, pai e filho), a serialização "em círculo" pode levar a um estouro de pilha (StackOverflowException) ou a uma exceção sobre referência cíclica. Em serializers JSON isso pode ser contornado com configurações, mas muitas vezes é melhor revisar a arquitetura do modelo.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION