CodeGym /Kursy /C# SELF /Dynamczne struktury: JObje...

Dynamczne struktury: JObject, JArray

C# SELF
Poziom 47 , Lekcja 3
Dostępny

1. Wprowadzenie

W poprzednich wykładach zawsze pracowaliśmy ze ściśle typowanymi strukturami. Na przykład mamy klasę Person, którą serializujemy do JSON i z powrotem:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Ale czasem nie znasz z góry struktury danych. Na przykład:

  • Piszesz parser dla zewnętrznego serwisu, gdzie struktura odpowiedzi nie jest stała.
  • Potrzebujesz wyciągnąć tylko część informacji, niekoniecznie wypełniać całą klasę.
  • Musisz edytować lub generować JSON "w locie", bazując na dynamicznych warunkach.

Tutaj wkraczają "dynamiczne struktury" — obiekty, które przechowują drzewo JSON jako zestaw kluczy i wartości, nie wymagając z góry opisanego C#-owego klasy.

Dlaczego najczęściej używa się Newtonsoft.Json

W .NET są dwaj główni gracze do pracy z JSON:

  • System.Text.Json — wbudowana biblioteka Microsoft (rozwija się od .NET Core 3.0).
  • Newtonsoft.Json (Json.NET) — popularna biblioteka, która dała światu C# takie klasy jak JObject i JArray.

W momencie pisania wykładu System.Text.Json wciąż nie oferuje pełnego odpowiednika JObject/JArray z tym samym wygodnictwem. Dlatego, jeśli często trzeba analizować lub modyfikować złożone/nieznane struktury JSON, częściej wybiera się Newtonsoft.Json.

Główne typy: JObject, JArray, JValue i rodzina JToken

  • JToken — typ bazowy dla wszystkich węzłów JSON.
  • JObject — obiekt JSON { ... }, zestaw par "klucz-wartość".
  • JArray — tablica [ ... ].
  • JValue — pojedyncza wartość (42, "tekst", true, null).

Idea jest prosta: parsujemy JSON — dostajemy "drzewo" z tych tokenów i możemy po nim chodzić, szukać, zmieniać, usuwać i dodawać elementy.

2. Odczyt nieznanego JSON

Załóżmy, że przyszło nam taki JSON i nie chcemy lub nie możemy z góry pisać dla niego klasy:

{
  "status": "ok",
  "amount": 150.5,
  "items": [
    {
      "name": "book",
      "qty": 1
    },
    {
      "name": "pen",
      "qty": 3
    }
  ]
}

Za pomocą Newtonsoft.Json możemy zamienić go na drzewo i badać w locie.

Przykład: czytanie JSON do JObject

using Newtonsoft.Json.Linq;

string json = @"{
  ""status"": ""ok"",
  ""amount"": 150.5,
  ""items"": [
    { ""name"": ""book"", ""qty"": 1 },
    { ""name"": ""pen"", ""qty"": 3 }
  ]
}";

// Parsujemy string i dostajemy drzewo
JObject root = JObject.Parse(json);

// Pobieramy właściwości jak ze słownika
string status = (string)root["status"]; // "ok"
double amount = (double)root["amount"]; // 150.5

// items — to tablica, czyli JArray
JArray items = (JArray)root["items"];

// Iterujemy po tablicy
foreach (JObject item in items)
{
    string name = (string)item["name"];
    int qty = (int)item["qty"];
    Console.WriteLine($"Towar: {name}, Ilość: {qty}");
}

Niesamowicie wygodne: żadnego deklarowania klas — możesz szybko wydłubać potrzebne kawałki.

3. Indeksatory i dynamiczny dostęp

Indeksatory:

  • Dla obiektu: root["status"], root["items"]
  • Dla tablicy: items[0], items[1]

Aby dostać wartość od razu w potrzebnym typie, użyj rzutowania (string), (int), (bool), (double) — biblioteka automatycznie konwertuje typ.

Jeśli danych może brakować, bądź ostrożny: dostęp do nieistniejącego klucza zwróci null, a jawne rzutowanie się wywali. Lepiej używać metod z weryfikacją:

if (root.TryGetValue("amount", out var token))
{
    double amount = token.Value<double>();
    // Taki sposób wygodniejszy: Value<T>() – od razu konwertuje
}

Wbudowane obiekty i tablice też czyta się prosto:

// Pobrać drugi towar
JObject secondItem = (JObject)root["items"][1];
string itemName = (string)secondItem["name"]; // "pen"

Można też dynamicznie:

dynamic droot = root;
Console.WriteLine(droot.status); // "ok"

Ale pamiętaj: z dynamic kompilator nie sprawdzi dostępu do pól — błędy wyjdą dopiero w czasie wykonywania.

4. Modyfikacja drzewa JSON w locie

Dodawanie elementów

root["currency"] = "RUB";        // Dodaliśmy nowe właściwość
items.Add(new JObject
{
    ["name"] = "eraser",
    ["qty"] = 2
});

Zmiana i usuwanie

root["status"] = "done";         // Zmieniliśmy wartość
items[0]["qty"] = 5;             // Zwiększyliśmy ilość pierwszego towaru
items.RemoveAt(1);               // Usunęliśmy drugi towar

Końcowe zapisanie do stringa

string modifiedJson = root.ToString();
// Albo root.ToString(Formatting.Indented) dla ładniejszego formatu

5. Tworzenie struktury JSON od zera

var person = new JObject
{
    ["name"] = "Alice",
    ["age"] = 22,
    ["languages"] = new JArray { "C#", "Python" },
    ["isStudent"] = true
};

Console.WriteLine(person.ToString(Newtonsoft.Json.Formatting.Indented));
{
  "name": "Alice",
  "age": 22,
  "languages": [
    "C#",
    "Python"
  ],
  "isStudent": true
}

Przydatne, jeśli trzeba zwrócić na zewnętrzne API tylko część danych lub złożyć JSON według warunków.

6. Jak złożyć własny obiekt z JObject

Czasem trzeba zmienić elastyczny JSON w ścisły obiekt C#. Opcje:

  • Zwykła deserializacja: JsonConvert.DeserializeObject<MyClass>(...).
  • Ręczne złożenie obiektu, wyciągając potrzebne pola z JObject.
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Załóżmy, że przyszło nam taki JSON:
string incoming = @"{ ""name"":""Bob"", ""age"":30, ""extraField"":true }";

JObject j = JObject.Parse(incoming);

// Składamy obiekt ręcznie — tylko potrzebne pola
var person = new Person
{
    Name = (string)j["name"],
    Age = (int)j["age"],
    // extraField nie jest nam potrzebne — i super!
};

7. Przykłady błędów i niuanse: typowe pułapki

Praca z dynamicznymi strukturami JSON jest elastyczna, ale są "podwodne skały":

  • Odwołanie do nieistniejącego klucza/elementu zwraca null; jawne rzutowanie do typu wartościowego spowoduje wyjątek.
  • Niezgodność oczekiwanego typu (oczekujesz obiektu, a przyszła wartość) — błąd rzutowania.
  • Do iteracji po polach obiektu używaj Properties():
foreach (var prop in root.Properties())
{
    Console.WriteLine($"Pole: {prop.Name}, Wartość: {prop.Value}");
}
  • W Newtonsoft.Json wygodnie pracuje się z zapytaniami podobnymi do LINQ (filtrowanie, wyszukiwanie):
var expensiveItems = items.Where(obj => (int)obj["qty"] > 2);
foreach (var item in expensiveItems)
    Console.WriteLine(item);

8. Typowe błędy przy pracy z JObject/JArray

Błąd nr 1: brak oczekiwanego pola albo niezgodność typu. Bardzo często developer zakłada, że w obiekcie będzie dane pole, ale go nie ma albo ma inny typ. Jeśli oczekujesz obiektu, a przychodzi liczba, przy rzutowaniu wystąpi wyjątek. Sprawdzaj istnienie i typ przed użyciem.

Błąd nr 2: odwoływanie się do pól zagnieżdżonych struktur bez sprawdzenia na null. Kiedy w JSON są zagnieżdżone obiekty, a klucze różnią się albo brakują, próba dostępu do nieistniejącego pola może spowodować awarię. Rób kontrolę na null przed czytaniem wartości zagnieżdżonych węzłów.

Błąd nr 3: rzutowanie do typu wartościowego gdy wartość jest null. Jeśli klucz istnieje, ale jego wartość to null, wyrażenie typu (int)j["age"] spowoduje wyjątek. Używaj Value<T>() — zwróci wartość domyślną (dla int to 0, dla stringów — null).

Błąd nr 4: nadmierne poleganie na obiektach dynamicznych zamiast jasnych modeli. Zbyt złożone struktury lepiej i bezpieczniej opisać klasami C#: to zmniejszy liczbę błędów i poprawi czytelność kodu. Dynamikę stosuj tam, gdzie struktura naprawdę jest nieznana lub bardzo się zmienia.

Teraz wiesz, jak szybko i bezpiecznie parsować, zmieniać i tworzyć struktury JSON przy pomocy JObject i JArray, nawet bez z góry opisanych modeli.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION