Menyesuaikan kontrak JSON
Pustaka System.Text.Json membuat kontrak JSON untuk setiap jenis .NET, yang menentukan bagaimana jenis harus diserialisasikan dan dideserialisasi. Kontrak berasal dari bentuk jenis, yang mencakup karakteristik seperti properti dan bidangnya dan apakah mengimplementasikan IEnumerable antarmuka atau IDictionary . Jenis dipetakan ke kontrak baik pada waktu proses menggunakan pantulan atau pada waktu kompilasi menggunakan generator sumber.
Mulai dari .NET 7, Anda dapat menyesuaikan kontrak JSON ini untuk memberikan kontrol lebih besar atas bagaimana jenis dikonversi menjadi JSON dan sebaliknya. Daftar berikut ini hanya memperlihatkan beberapa contoh jenis kustomisasi yang dapat Anda buat untuk serialisasi dan deserialisasi:
- Menserialisasikan bidang dan properti privat.
- Mendukung beberapa nama untuk satu properti (misalnya, jika versi pustaka sebelumnya menggunakan nama yang berbeda).
- Abaikan properti dengan nama, jenis, atau nilai tertentu.
- Membedakan antara nilai eksplisit
null
dan kurangnya nilai dalam payload JSON. - Atribut dukungan System.Runtime.Serialization , seperti DataContractAttribute. Untuk informasi selengkapnya, lihat Atribut System.Runtime.Serialization.
- Berikan pengecualian jika JSON menyertakan properti yang bukan bagian dari jenis target. Untuk informasi selengkapnya, lihat Menangani anggota yang hilang.
Cara ikut serta
Ada dua cara untuk menyambungkan ke penyesuaian. Keduanya melibatkan mendapatkan resolver, yang tugasnya adalah menyediakan JsonTypeInfo instans untuk setiap jenis yang perlu diserialisasikan.
Dengan memanggil DefaultJsonTypeInfoResolver() konstruktor untuk mendapatkan JsonSerializerOptions.TypeInfoResolver dan menambahkan tindakan kustom Anda ke propertinyaModifiers.
Contohnya:
JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver { Modifiers = { MyCustomModifier1, MyCustomModifier2 } } };
Jika Anda menambahkan beberapa pengubah, pengubah tersebut akan dipanggil secara berurutan.
Dengan menulis resolver kustom yang mengimplementasikan IJsonTypeInfoResolver.
- Jika jenis tidak ditangani, IJsonTypeInfoResolver.GetTypeInfo harus kembali
null
untuk jenis tersebut. - Anda juga dapat menggabungkan resolver kustom Anda dengan orang lain, misalnya, resolver default. Resolver akan dikueri secara berurutan sampai nilai non-null JsonTypeInfo dikembalikan untuk jenis tersebut.
- Jika jenis tidak ditangani, IJsonTypeInfoResolver.GetTypeInfo harus kembali
Aspek yang dapat dikonfigurasi
Properti JsonTypeInfo.Kind menunjukkan bagaimana pengonversi menserialisasikan jenis tertentu—misalnya, sebagai objek atau sebagai array, dan apakah propertinya diserialisasikan. Anda dapat mengkueri properti ini untuk menentukan aspek kontrak JSON jenis mana yang dapat Anda konfigurasikan. Ada empat jenis yang berbeda:
JsonTypeInfo.Kind |
Deskripsi |
---|---|
JsonTypeInfoKind.Object | Pengonversi akan menserialisasikan jenis ke dalam objek JSON dan menggunakan propertinya. Jenis ini digunakan untuk sebagian besar jenis kelas dan struktur dan memungkinkan fleksibilitas terbanyak. |
JsonTypeInfoKind.Enumerable | Pengonversi akan menserialisasikan jenis ke dalam array JSON. Jenis ini digunakan untuk jenis seperti List<T> dan array. |
JsonTypeInfoKind.Dictionary | Pengonversi akan menserialisasikan jenis ke dalam objek JSON. Jenis ini digunakan untuk jenis seperti Dictionary<K, V> . |
JsonTypeInfoKind.None | Pengonversi tidak menentukan bagaimana konverter akan menserialisasikan jenis atau properti apa yang JsonTypeInfo akan digunakannya. Jenis ini digunakan untuk jenis seperti System.Object, int , dan string , dan untuk semua jenis yang menggunakan pengonversi kustom. |
Pengubah
Pengubah adalah Action<JsonTypeInfo>
metode atau dengan JsonTypeInfo parameter yang mendapatkan status kontrak saat ini sebagai argumen dan membuat modifikasi pada kontrak. Misalnya, Anda dapat melakukan iterasi melalui properti yang telah diisi sebelumnya pada yang ditentukan JsonTypeInfo untuk menemukan properti yang Anda minati lalu memodifikasi propertinya JsonPropertyInfo.Get (untuk serialisasi) atau JsonPropertyInfo.Set properti (untuk deserialisasi). Atau, Anda dapat membuat properti baru menggunakan JsonTypeInfo.CreateJsonPropertyInfo(Type, String) dan menambahkannya ke JsonTypeInfo.Properties koleksi.
Tabel berikut ini memperlihatkan modifikasi yang bisa Anda buat dan cara mencapainya.
Modifikasi | Berlaku JsonTypeInfo.Kind |
Cara mencapainya | Contoh |
---|---|---|---|
Mengkustomisasi nilai properti | JsonTypeInfoKind.Object |
JsonPropertyInfo.Get Ubah delegasi (untuk serialisasi) atau JsonPropertyInfo.Set delegasikan (untuk deserialisasi) untuk properti . | Menaikkan nilai properti |
Menambahkan atau menghapus properti | JsonTypeInfoKind.Object |
Tambahkan atau hapus item dari JsonTypeInfo.Properties daftar. | Menserialisasikan bidang privat |
Menserialisasikan properti secara kondisional | JsonTypeInfoKind.Object |
JsonPropertyInfo.ShouldSerialize Ubah predikat untuk properti . | Mengabaikan properti dengan jenis tertentu |
Menyesuaikan penanganan angka untuk jenis tertentu | JsonTypeInfoKind.None |
JsonTypeInfo.NumberHandling Ubah nilai untuk jenis tersebut. | Perbolehkan nilai int menjadi string |
Contoh: Menaikkan nilai properti
Pertimbangkan contoh berikut di mana pengubah meningkatkan nilai properti tertentu pada deserialisasi dengan memodifikasi delegasinya JsonPropertyInfo.Set . Selain menentukan pengubah, contohnya juga memperkenalkan atribut baru yang digunakannya untuk menemukan properti yang nilainya harus dinaikkan. Ini adalah contoh penyesuaian properti.
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace Serialization
{
// Custom attribute to annotate the property
// we want to be incremented.
[AttributeUsage(AttributeTargets.Property)]
class SerializationCountAttribute : Attribute
{
}
// Example type to serialize and deserialize.
class Product
{
public string Name { get; set; } = "";
[SerializationCount]
public int RoundTrips { get; set; }
}
public class SerializationCountExample
{
// Custom modifier that increments the value
// of a specific property on deserialization.
static void IncrementCounterModifier(JsonTypeInfo typeInfo)
{
foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties)
{
if (propertyInfo.PropertyType != typeof(int))
continue;
object[] serializationCountAttributes = propertyInfo.AttributeProvider?.GetCustomAttributes(typeof(SerializationCountAttribute), true) ?? Array.Empty<object>();
SerializationCountAttribute? attribute = serializationCountAttributes.Length == 1 ? (SerializationCountAttribute)serializationCountAttributes[0] : null;
if (attribute != null)
{
Action<object, object?>? setProperty = propertyInfo.Set;
if (setProperty is not null)
{
propertyInfo.Set = (obj, value) =>
{
if (value != null)
{
// Increment the value by 1.
value = (int)value + 1;
}
setProperty (obj, value);
};
}
}
}
}
public static void RunIt()
{
var product = new Product
{
Name = "Aquafresh"
};
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { IncrementCounterModifier }
}
};
// First serialization and deserialization.
string serialized = JsonSerializer.Serialize(product, options);
Console.WriteLine(serialized);
// {"Name":"Aquafresh","RoundTrips":0}
Product deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
Console.WriteLine($"{deserialized.RoundTrips}");
// 1
// Second serialization and deserialization.
serialized = JsonSerializer.Serialize(deserialized, options);
Console.WriteLine(serialized);
// { "Name":"Aquafresh","RoundTrips":1}
deserialized = JsonSerializer.Deserialize<Product>(serialized, options)!;
Console.WriteLine($"{deserialized.RoundTrips}");
// 2
}
}
}
Perhatikan dalam output bahwa nilai bertahap RoundTrips
setiap kali Product
instans dideserialisasi.
Contoh: Menserialisasikan bidang privat
Secara default, System.Text.Json
mengabaikan bidang dan properti privat. Contoh ini menambahkan atribut seluruh kelas baru, JsonIncludePrivateFieldsAttribute
, untuk mengubah default tersebut. Jika pengubah menemukan atribut pada jenis, pengubah menambahkan semua bidang privat pada jenis sebagai properti baru ke JsonTypeInfo.
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Serialization
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class JsonIncludePrivateFieldsAttribute : Attribute { }
[JsonIncludePrivateFields]
public class Human
{
private string _name;
private int _age;
public Human()
{
// This constructor should be used only by deserializers.
_name = null!;
_age = 0;
}
public static Human Create(string name, int age)
{
Human h = new()
{
_name = name,
_age = age
};
return h;
}
[JsonIgnore]
public string Name
{
get => _name;
set => throw new NotSupportedException();
}
[JsonIgnore]
public int Age
{
get => _age;
set => throw new NotSupportedException();
}
}
public class PrivateFieldsExample
{
static void AddPrivateFieldsModifier(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
return;
if (!jsonTypeInfo.Type.IsDefined(typeof(JsonIncludePrivateFieldsAttribute), inherit: false))
return;
foreach (FieldInfo field in jsonTypeInfo.Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic))
{
JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name);
jsonPropertyInfo.Get = field.GetValue;
jsonPropertyInfo.Set = field.SetValue;
jsonTypeInfo.Properties.Add(jsonPropertyInfo);
}
}
public static void RunIt()
{
var options = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { AddPrivateFieldsModifier }
}
};
var human = Human.Create("Julius", 37);
string json = JsonSerializer.Serialize(human, options);
Console.WriteLine(json);
// {"_name":"Julius","_age":37}
Human deserializedHuman = JsonSerializer.Deserialize<Human>(json, options)!;
Console.WriteLine($"[Name={deserializedHuman.Name}; Age={deserializedHuman.Age}]");
// [Name=Julius; Age=37]
}
}
}
Tip
Jika nama bidang privat Anda dimulai dengan garis bawah, pertimbangkan untuk menghapus garis bawah dari nama saat Anda menambahkan bidang sebagai properti JSON baru.
Contoh: Mengabaikan properti dengan jenis tertentu
Mungkin model Anda memiliki properti dengan nama atau jenis tertentu yang tidak ingin Anda ekspos ke pengguna. Misalnya, Anda mungkin memiliki properti yang menyimpan kredensial atau beberapa informasi yang tidak berguna untuk dimiliki dalam payload.
Contoh berikut menunjukkan cara memfilter properti dengan jenis tertentu, SecretHolder
. Ini dilakukan dengan menggunakan IList<T> metode ekstensi untuk menghapus properti apa pun yang memiliki jenis yang ditentukan dari JsonTypeInfo.Properties daftar. Properti yang difilter benar-benar menghilang dari kontrak, yang berarti System.Text.Json
tidak melihatnya baik selama serialisasi atau deserialisasi.
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace Serialization
{
class ExampleClass
{
public string Name { get; set; } = "";
public SecretHolder? Secret { get; set; }
}
class SecretHolder
{
public string Value { get; set; } = "";
}
class IgnorePropertiesWithType
{
private readonly Type[] _ignoredTypes;
public IgnorePropertiesWithType(params Type[] ignoredTypes)
=> _ignoredTypes = ignoredTypes;
public void ModifyTypeInfo(JsonTypeInfo ti)
{
if (ti.Kind != JsonTypeInfoKind.Object)
return;
ti.Properties.RemoveAll(prop => _ignoredTypes.Contains(prop.PropertyType));
}
}
public class IgnoreTypeExample
{
public static void RunIt()
{
var modifier = new IgnorePropertiesWithType(typeof(SecretHolder));
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { modifier.ModifyTypeInfo }
}
};
ExampleClass obj = new()
{
Name = "Password",
Secret = new SecretHolder { Value = "MySecret" }
};
string output = JsonSerializer.Serialize(obj, options);
Console.WriteLine(output);
// {"Name":"Password"}
}
}
public static class ListHelpers
{
// IList<T> implementation of List<T>.RemoveAll method.
public static void RemoveAll<T>(this IList<T> list, Predicate<T> predicate)
{
for (int i = 0; i < list.Count; i++)
{
if (predicate(list[i]))
{
list.RemoveAt(i--);
}
}
}
}
}
Contoh: Perbolehkan nilai int menjadi string
Mungkin input JSON Anda dapat berisi tanda kutip di sekitar salah satu jenis numerik tetapi tidak pada yang lain. Jika Anda memiliki kontrol atas kelas, Anda dapat menempatkan JsonNumberHandlingAttribute pada jenis untuk memperbaiki ini, tetapi Anda tidak. Sebelum .NET 7, Anda harus menulis pengonversi kustom untuk memperbaiki perilaku ini, yang memerlukan penulisan sedikit kode. Dengan menggunakan kustomisasi kontrak, Anda dapat menyesuaikan perilaku penanganan angka untuk semua jenis.
Contoh berikut mengubah perilaku untuk semua int
nilai. Contohnya dapat dengan mudah disesuaikan untuk diterapkan ke jenis apa pun atau untuk properti tertentu dari jenis apa pun.
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace Serialization
{
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
public class AllowIntsAsStringsExample
{
static void SetNumberHandlingModifier(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Type == typeof(int))
{
jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
}
}
public static void RunIt()
{
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { SetNumberHandlingModifier }
}
};
// Triple-quote syntax is a C# 11 feature.
Point point = JsonSerializer.Deserialize<Point>("""{"X":"12","Y":"3"}""", options)!;
Console.WriteLine($"({point.X},{point.Y})");
// (12,3)
}
}
}
Tanpa pengubah untuk mengizinkan nilai pembacaan int
dari string, program akan diakhir dengan pengecualian:
Pengecualian tidak dapat ditangani. System.Text.Json.JsonException: Nilai JSON tidak dapat dikonversi ke System.Int32. Jalur: $. X | LineNumber: 0 | BytePositionInLine: 9.
Cara lain untuk menyesuaikan serialisasi
Selain menyesuaikan kontrak, ada cara lain untuk memengaruhi perilaku serialisasi dan deserialisasi, termasuk yang berikut:
- Dengan menggunakan atribut yang berasal dari JsonAttribute, misalnya, JsonIgnoreAttribute dan JsonPropertyOrderAttribute.
- Dengan memodifikasi JsonSerializerOptions, misalnya, untuk mengatur kebijakan penamaan atau menserialisasikan nilai enumerasi sebagai string, bukan angka.
- Dengan menulis pengonversi kustom yang melakukan pekerjaan aktual menulis JSON dan, selama deserialisasi, membangun objek.
Kustomisasi kontrak adalah peningkatan atas penyesuaian yang sudah ada sebelumnya ini karena Anda mungkin tidak memiliki akses ke jenis untuk menambahkan atribut. Selain itu, menulis pengonversi kustom rumit dan merugikan performa.