1. Po co są atrybuty przy serializacji?
Kiedy serializujesz obiekty do JSON lub XML, dzieje się coś podobnego do skanowania twojego pokoju robotem-sprzątaczem. Wszystko, co wyraźnie leży na widoku (publiczne właściwości/pola), trafia do „worka” (do pliku lub stringa), a reszta jest ignorowana. Ale czasami nie chcesz, żeby robot pokazał zawartość twojej torby, albo chcesz ładniej nazwać rzeczy — na przykład nie po-rosyjsku, a po-angielsku.
Tu wchodzą atrybuty! Pozwalają sterować procesem serializacji: ukrywać zbędne rzeczy, zmieniać nazwy, nakładać porządek, ignorować części obiektu albo mówić serializatorowi, że masz specjalne reguły. To jak naklejki na rzeczach: „nie dotykać”, „ważne”, „zmienić nazwę w JSON”.
Typowe zadania kontroli serializacji
- Zmień nazwę właściwości lub pola w dokumencie wyjściowym (np. w JSON zamiast FirstName zrobić "first_name")
- Ukryć niektóre pola/właściwości przed serializacją lub deserializacją (np. hasła, wewnętrzne liczniki)
- Sterować traktowaniem wartości domyślnych lub null-wartości
- Ustawić kolejność elementów (ważne dla XML)
- Opisać dodatkowe parametry (atrybuty) dla elementów XML
Każda platforma serializacyjna — czy to System.Text.Json, Newtonsoft.Json czy XmlSerializer — używa swoich atrybutów do tych celów.
2. Atrybuty dla System.Text.Json: nowoczesny i modny
Klasyczny serializator JSON w systemie .NET obsługuje niezły zestaw atrybutów, które żyją w przestrzeni nazw System.Text.Json.Serialization.
Najbardziej użyteczne atrybuty:
| Atrybut | Do czego służy | Przykład użycia |
|---|---|---|
|
Zmienia nazwę właściwości w JSON | |
|
Całkowicie wyklucza właściwość z serializacji | |
|
Serializuje publiczne pole (a nie tylko właściwość) | |
|
Ignorować właściwość w określonych warunkach | |
Przykłady użycia:
using System.Text.Json.Serialization;
public class Person
{
[JsonPropertyName("first_name")]
public string FirstName { get; set; } // Nazwa w JSON będzie 'first_name'
[JsonIgnore]
public string Password { get; set; } // Nie trafi do JSON
[JsonPropertyName("born_year")]
public int? YearOfBirth { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Nickname { get; set; } // Jeśli null — nie trafi do JSON
}
Zserializujmy taką osobę:
var person = new Person
{
FirstName = "Ivan",
Password = "123456",
YearOfBirth = 2000,
Nickname = null
};
string json = JsonSerializer.Serialize(person);
// json: {"first_name":"Ivan","born_year":2000}
Zauważ: i hasło, i ksywka zniknęły z JSON-a! (Hasło — bo jest zawsze ignorowane, a ksywka — tylko jeśli jest null).
Dlaczego to ważne? W praktyce często nie chcesz, żeby strona kliencka (albo ktoś niepowołany!) zobaczył krytyczne dane jak hasła, tokeny, liczniki wewnętrzne, timestampy. Dzięki [JsonIgnore] i podobnym atrybutom zrobisz to jedną linią.
3. Atrybuty dla Newtonsoft.Json: lider elastyczności
Jeśli pracujesz z Newtonsoft.Json (dokumentacja tutaj), masz jeszcze więcej możliwości (i trochę prostsze API, jeśli pochodzisz ze starych projektów).
Główne atrybuty:
| Atrybut | Przeznaczenie |
|---|---|
|
Ustawia nazwę właściwości w JSON, oraz kolejność, obowiązkowość itp. |
|
Wyklucza pole/właściwość z serializacji |
|
Wymaga obecności przy deserializacji |
|
Pozwala wskazać własny konwerter dla skomplikowanych przypadków |
|
Steruje serializacją wartości domyślnych |
Przykład w praktyce:
using Newtonsoft.Json;
public class UserProfile
{
[JsonProperty("login")]
public string Username { get; set; }
[JsonIgnore]
public string InternalNotes { get; set; }
[JsonProperty(Required = Required.Always)]
public string Email { get; set; }
}
Dodatkowe możliwości JsonProperty — obowiązkowość (Required), kolejność (Order) itd. Na przykład:
[JsonProperty("id", Order = 1, Required = Required.Always)]
public int Id { get; set; }
4. Atrybuty dla XML: styl "classic"
XmlSerializer używa całego arsenału własnych atrybutów z przestrzeni nazw System.Xml.Serialization.
Najczęściej spotykane:
| Atrybut | Do czego służy |
|---|---|
|
Element w XML z inną nazwą |
|
Mapuje właściwość na atrybut elementu XML |
|
Wyklucza właściwość lub pole z serializacji do XML |
|
Dla kolekcji — ustawia nazwę tablicy XML |
|
Dla kolekcji — ustawia nazwę elementu tablicy |
|
Zmienia nazwę korzeniowego taga XML |
Przykład w praktyce:
using System.Xml.Serialization;
[XmlRoot("human")]
public class Person
{
[XmlElement("firstname")]
public string Name { get; set; }
[XmlAttribute("years")]
public int Age { get; set; }
[XmlIgnore]
public string Secret { get; set; }
}
var person = new Person { Name = "Anna", Age = 32, Secret = "42" };
Po serializacji XML będzie mniej więcej taki:
<human years="32"><firstname>Anna</firstname></human>
Zwróć uwagę, że pole Secret nie trafiło do XML, a Age zostało zserializowane jako atrybut, a nie jako zagnieżdżony tag.
5. Przydatne niuanse
Pod maską: jak działają atrybuty
Kiedy serializator napotka twoją klasę, dosłownie „czyta” ją przez reflection (magia .NET, więcej w System.Reflection). Serializator odczytuje metadane: na przykład, czy właściwość ma atrybut JsonIgnore albo XmlElement. W zależności od tego dodaje dane do końcowego dokumentu (albo je pomija).
To wygodny sposób oddzielić „schemat danych” od logiki biznesowej. Twoja klasa — to twoja logika biznesowa, a atrybuty — to w zasadzie paszport do serializacji.
Tabela zgodności kluczowych atrybutów
| Funkcja | System.Text.Json | Newtonsoft.Json | XmlSerializer |
|---|---|---|---|
| Zmień nazwę | |
|
|
| Ignoruj | |
|
|
| Własny format | |
|
— (przez IXmlSerializable, boli) |
| Korzeń obiektu | — | — | |
| Kolekcja | — | — | |
6. Zaawansowane scenariusze
Czasem potrzebujesz serializować obiekt inaczej niż potrafi standardowy serializer. Na przykład chcesz przechowywać datę jako UNIX timestamp, a nie w standardowym formacie ISO. W takim przypadku są atrybuty, które pozwalają podłączyć własne konwertery.
W System.Text.Json:
public class UnixDateTimeConverter : JsonConverter<DateTime>
{
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime;
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> writer.WriteNumberValue(new DateTimeOffset(value).ToUnixTimeSeconds());
}
public class LogEntry
{
[JsonConverter(typeof(UnixDateTimeConverter))]
public DateTime EventTime { get; set; }
}
Teraz przy serializacji pole EventTime zamieni się w liczbę, a nie string z datą.
W Newtonsoft.Json:
public class BoolToYesNoConverter : JsonConverter<bool>
{
public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer)
=> writer.WriteValue(value ? "yes" : "no");
public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer)
=> (string)reader.Value == "yes";
}
public class Answer
{
[JsonConverter(typeof(BoolToYesNoConverter))]
public bool IsCorrect { get; set; }
}
Teraz wartość bool będzie serializowana jako "yes" lub "no", a z powrotem zamieniana na true lub false.
7. Cechy i pułapki
Kiedy dodajesz atrybuty, pamiętaj: różne serializatory używają różnych atrybutów. Jeśli pracujesz i z JSON, i z XML (albo — co gorsza — jednocześnie z Newtonsoft.Json i System.Text.Json), nie zapomnij dodać obu potrzebnych atrybutów, inaczej czeka cię niespodzianka.
Standardowa nazwa właściwości będzie użyta przez serializator, chyba że ją nadpiszesz przez atrybut.
Uważaj przy dziedziczeniu: klasy potomne dziedziczą publiczne pola/właściwości i jeśli na rodzicu był atrybut, będzie on stosowany także w klasie potomnej. To często zaskakuje, ale tak jest zaprojektowane.
Typowy błąd: Często zdarza się, że deweloperzy przez przypadek oznaczą pole, które musi trafić do serializacji, atrybutem JsonIgnore (albo odwrotnie — zapomną zignorować wrażliwe dane). Albo np. dodają XmlElement do prywatnego pola — i dziwią się, czemu serializator go nie widzi (XmlSerializer obsługuje tylko public członków!).
GO TO FULL VERSION