.NET Core 3.0 óta a keretrendszer beépÃtetten tartalmaz egy JSON szerializálót, ami a System.Text.Json névtérben található. Használata hasonló a Newtonsoft JSON szerializálóhoz. A szerializálásra és deszerializálásra itt a JsonSerializer statikus osztályt használhatjuk.
string Serialize<TValue> (TValue value, JsonSerializerOptions? options = default);
void Serialize<TValue> (Stream utf8Json, TValue value, JsonSerializerOptions? options = default);
Szerializáció esetén megadhatunk egy JsonSerializerOptions példányt, ami a szerializáló működését befolyásolja. A JsonSerializerOptions lehetÅ‘séget biztosÃt arra, hogy a null értékek ne kerüljenek szerializálásra (IgnoreNullValues), megadhatjuk, hogy a JSON dokumentumot formázni szeretnénk-e, (WriteIndented), illetve sok egyéb beállÃtást is.
Deszerializálni itt a Deserialize metódussal tudunk:
TValue? Deserialize<TValue> (string json, JsonSerializerOptions? options = default);
TValue? Deserialize<TValue> (Stream utf8Json, JsonSerializerOptions? options = default);
Itt is opcionálisan megadhatunk egy JsonSerializerOptions példányt, ami a deszerializáló működését befolyásolja.
BeállÃtások
Ha úgy döntenénk, hogy nem az alap beállÃtásokat használjuk, akkor szerializáció és deszerializáció közben is ugyan azokat a beállÃtásokat használjuk. Ha ezek eltérnek szerializáció és deszerializáció közben, akkor könnyen kivételre futhat a visszaolvasás.
Az osztály fontosabb beállÃtásai:
public bool IncludeFields { get; set; }
public bool IgnoreReadOnlyFields { get; set; }
Osztályon belüli field-ek szerializációját befolyásolja az IncludeFields tulajdonság. Alapértelmezetten ki van kapcsolva, vagyis csak az osztály tulajdonságai szerializálódnak. Az IgnoreReadOnlyFields bekapcsolt IncludeFields esetén jön jól, mivel ebben az esetben a readonly módosÃtóval ellátott adattagok nem szerializálódnak. A kettÅ‘t együtt érdemes használni.
public bool IgnoreReadOnlyProperties { get; set; }
Csak olvasható (get) property-k kihagyása. Alapértelmezetten kikapcsolt.
public bool WriteIndented { get; set; }
Formázott kimenet Ãrása, vagy sem. Alapértelmezetten kikapcsolt, vagyis a kimeneti JSON sortörések és formázások nélkül kerül kiÃrásra, ami hálózati átvitel esetén hasznos. Azonban ha lemezre Ãrjuk a fájlt és feltétel, hogy kézzel is könnyen szerkeszthetÅ‘ legyen a fájl, akkor érdemes bekapcsolni.
public JsonNamingPolicy? PropertyNamingPolicy { get; set; }
KiÃrt adattagok elnevezéséhez használt beállÃtás. C# esetén a tulajdonságok nevei PascalCase-t követnek, de JavaScript és ebbÅ‘l adódóan JSON esetén a camelCase a bevett szokás. A JsonNamingPolicy egy absztakt osztály, ami segÃtségével saját elnevezési szabályokat is implementálhatunk, vagy használhatjuk az osztály CamelCase statikus adattagját, ha camelCase kell. Ezt érdemes beállÃtani, ha a szerializált adat JavaScript-bÅ‘l lesz feldolgozva. Ha nem állÃtunk neki értéket, akkor marad PascalCase beállÃtáson.
public bool PropertyNameCaseInsensitive { get; set; }
Ez a beállÃtás deszerializáció során alkalmazott. Bekapcsolt állapotában nem számÃt különbségnek, hogy kis vagy nagybetűk szerepelnek egy tulajdonság nevében. Ez hasznos lehet, ha kézzel szerkesztett bemenetet dolgozunk fel, de a teljesÃtményre negatÃv hatása van.
JsonIgnoreCondition DefaultIgnoreCondition { get; set; }
Azt befolyásolja, hogy az alapértelmezett értékkel rendelkezÅ‘ (pl. egy int esetén 0, vagy string esetén null) tulajdonság szerializálva legyen-e vagy sem. A JsonIgnoreCondition egy enum tÃpus, ami az alábbi értékeket veheti fel:
-
Never
AlapbeállÃtás. A tulajdonság mindig szerializálva és deszerializálva lesz.
-
WhenWritingDefault
A tulajdonság kihagyásra kerül, ha az értéke megegyezik a tÃpus alapértelmezett értékével.
-
WhenWritingNull
A tulajdonság kihagyásra kerül, ha az értéke
null.
public IList<JsonConverter> Converters { get; }
A szerializáció során használt konvertereket határozza meg.
TÃpus konverterek
Mivel a JSON a JavaScript tÃpusrendszerét örökli, közel sem rendelkezik olyan széleskörű tÃpusrendszerrel, mint a .NET és a C#. Ez a legtöbb esetben nem okoz problémát, de összetett tÃpusok kezelése esetén okozhat fejfájást. Például ha enum értékekkel dolgozunk. JSON esetén nincs enum, ezért ezek az értékek számként Ãródnak ki. Azonban enum értékek szám szerinti kiÃrása nem a legjobb megoldás.
Mégpedig azért, mert ha az enum tÃpusunkban nincs az értékeknek explicit szám megfeleltetés, akkor a kiÃrt fájlban a 12-es index érték elég kritikus, arról nem is beszélve, hogy mi van akkor, ha késÅ‘bb bÅ‘vül az enum és nem a végén bÅ‘vÃti a programozó? Ebben az esetben a 12-es kiÃrt érték már nem biztos, hogy azt jelenti, amit kÃÃrtunk. Éppen ezért jobb lenne ezen tÃpusokat szövegesen, név alapján kiÃrni.
Itt jönnek képbe a tÃpus konverterek, amik segÃtségével bármilyen összetett tÃpust tudunk egy primitÃv JSON tÃpusra konvertálni. Jelen esetben például szöveggé.
A korábban emlÃtett enum példára gondoltak a .NET fejlesztÅ‘k, ezért van rá beépÃtett konverter, aminek a neve JsonStringEnumConverter, Ãgy ezt nem kell magunknak lefejleszteni. Azonban számos tÃpus van, amit hasonlóan egy soros szövegként is tárolhatunk, ahelyett, hogy komplett objektumként szerializálnánk. Ilyen például a CultureInfo, amit egy elÅ‘re meghatározott int szám alapján is helyre tudunk állÃtani, vagy név alapján is. 1
Ahhoz, hogy tÃpus konvertert Ãrjunk, több lehetÅ‘ségünk is van. A legegyszerűbb azonban, ha a JsonConverter<T> tÃpusból származunk le.
Ez az osztály két metódussal rendelkezik, amit meg kell valósÃtanunk:
T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
A Read metódus a tÃpus felolvasásakor fog meghÃvódni, mÃg a Write Ãráskor. A metódusok esetén kapott Utf8JsonReader és Utf8JsonWriter objektumok JSON tokenek2 (szövegek, számok, azonosÃtók) olvasását és Ãrását teszik lehetÅ‘vé.
Nézzünk egy példát, ami segÃtségével egy CultureInfo példány szerializálható szövegként:
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
public override CultureInfo? Read(ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
return new CultureInfo(reader.GetString()!);
}
public override void Write(Utf8JsonWriter writer,
CultureInfo value,
JsonSerializerOptions options)
{
writer.WriteStringValue(value.Name);
}
}
Példaprogram
using System;
using System.Globalization;
using System.Text.Json.Serialization;
using System.Text.Json;
namespace JsonConvertPelda
{
//Osztály a CultureInfo osztállyal
//Ezt szerializáljuk
public class Class
{
public CultureInfo Culture { get; init; }
public Class()
{
Culture = CultureInfo.InvariantCulture;
}
}
//Konverter
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
public override CultureInfo? Read(ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
return new CultureInfo(reader.GetString()!);
}
public override void Write(Utf8JsonWriter writer,
CultureInfo value,
JsonSerializerOptions options)
{
writer.WriteStringValue(value.Name);
}
}
internal static class Program
{
private static void Main(string[] args)
{
Class toSerialize = new Class
{
Culture = new CultureInfo("hu-HU")
};
Console.Write("Serializáció előtt: ");
Console.WriteLine(toSerialize.Culture.DisplayName);
Console.Write("Konverter nélkül: ");
try
{
//Konverter nélkül megpróbáljuk szerializálni
string withoutConverter = JsonSerializer.Serialize(toSerialize);
Console.WriteLine(withoutConverter);
}
catch (Exception)
{
//Nem fog sikerülni
Console.WriteLine($"Nem sikerült");
}
//Konverter beállÃtása
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
options.Converters.Add(new CultureInfoConverter());
//szerializáció
string withConverter = JsonSerializer.Serialize(toSerialize, options);
Console.Write("Konverterrel: ");
Console.WriteLine(withConverter);
//Deszerializáció és teszt:
Class? read = JsonSerializer.Deserialize<Class>(withConverter, options);
Console.Write("Deszerializáció után: ");
Console.WriteLine(read?.Culture.DisplayName);
}
}
}
A program kimenete:
Serializáció elÅ‘tt: magyar (Magyarország)
Konverter nélkül: Nem sikerült
Konverterrel: {
"Culture": "hu-HU"
}
Deszerializáció után: magyar (Magyarország)
Polimorfikus szerializáció
A System.Text.Json szerializáló elÅ‘nye, hogy a 7.0-ás változattól támogatja a polimorfikus szerializációt is. Ez azt jelenti, hogy ha van egy osztály struktúránk és az Å‘st szerializáljuk, akkor nem csak az Å‘stÃpusban megtalálható tulajdonságok szerializálódnak, hanem a leszármazott osztályok tulajdonságai is és deszerializációkor pedig megfelelÅ‘en helyreállnak. Az alábbi példa ezt szemlélteti:
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace JsonPolimorph
{
[JsonDerivedType(typeof(Point3D), typeDiscriminator: "3d")]
public class Point2D
{
public float X { get; init; }
public float Y { get; init; }
public override string ToString()
=> $"x: {X}; y:{Y}";
}
public class Point3D : Point2D
{
public float Z { get; init; }
public override string ToString()
=> $"x: {X}; y:{Y}; z: {Z}";
}
internal static class Program
{
private static void Main(string[] args)
{
Point2D[] array = new[]
{
new Point2D { X = 1, Y = 2 },
new Point3D { X = 3, Y = 4, Z = 5 },
};
string json = JsonSerializer.Serialize(array, new JsonSerializerOptions
{
WriteIndented = true
});
Console.WriteLine(json);
Point2D[]? deserialized = JsonSerializer.Deserialize<Point2D[]>(json);
if (deserialized != null)
{
foreach (var item in deserialized)
{
Console.WriteLine(item);
}
}
}
}
}
A program kimenete:
[
{
"X": 1,
"Y": 2
},
{
"$type": "3d",
"Z": 5,
"X": 3,
"Y": 4
}
]
x: 1; y:2
x: 3; y:4; z: 5
A példa kimenetébÅ‘l látszódik, hogy ugyan Point2D tÃpusú tömböt szerializálunk, deszerializációkor mégis visszakapjuk az eredeti kollekciót, mintha mi sem történt volna.
A Point2D Å‘sosztályon egy JsonDerivedType attribútum lett megadva, ami a kiszerializált JSON dokumentumban a $type mezÅ‘t hozzáadja a leszármazott objektumunk szerializációjakor. Ezen typeDiscriminator azonosÃtó segÃtségével tudja deszerializációkor a rendszer, hogy milyen objektumot is hozzon létre. Ha ennek az attribútumnak a megadását elmulasztanánk, akkor numerikus azonosÃtókat használna a rendszer, amivel ugyanúgy működne, de emberileg nehezebben olvasható lenne a JSON dokumentum. Ez nem feltétlen baj, de hibakereséshez nagy segÃtség tud lenni, ha valami szöveges typeDiscriminator értéket használunk. Itt megjegyezném, hogy ugyan bármilyen szöveget megadhatunk, de van, amit nem érdemes. Például a typeDiscriminator értéke sose reprezentálja közvetlenül a kódunkban a tÃpus nevét, mert ez lehet biztonsági kockázat: információt árul el a programunk belsÅ‘ struktúrájáról.
Ha több örökölt tÃpus van, amit szerializálni szeretnénk, akkor mindegyikhez egy JsonDerivedType attribútumot fel kell vennünk.
-
A Windows és a .NET által támogatott Language ID-k numerikus és szöveges változatait a https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c cÃmen található dokumentum részletezi.↩
-
A JSON szabvány által elfogadott karakterek, jelölÅ‘k: https://www.json.org/json-en.html↩