Những khó khăn khi serialize union và closed class hierarchy với System.Text.Json

Đây là bài thứ bảy trong loạt bài: Khám phá .NET 11 preview.

  1. Phần 1 – Chạy background tasks trong Blazor với Web Workers
  2. Phần 2 – .NET (OK, C#) cuối cùng cũng có union types 🎉
  3. Phần 3 – Tránh ToString() allocations với StringBuilder.MoveChunks
  4. Phần 4 – Closed class hierarchies
  5. Phần 5 – Cải thiện việc đọc Process outputs
  6. Phần 6 – Bảo vệ CSRF tự động dựa trên Fetch Metadata headers
  7. Phần 7 – Những khó khăn khi serialize union và closed class hierarchy với System.Text.Json (bài viết này)

.NET 11 và C# 15 đang thêm hỗ trợ cho cả union types và closed class hierarchies. Trong bài viết này, tôi sẽ xem xét cách System.Text.Json xử lý các loại hierarchy mới này, sự khác biệt giữa chúng, cũng như một số “cạnh sắc” cần lưu ý. Có lẽ chỉ mình tôi, nhưng tôi thấy toàn bộ trải nghiệm khá đau đớn.

Tôi bắt đầu bằng cách tổng quan về union types và closed class hierarchies. Sau đó, tôi tạo các ví dụ đơn giản cho từng loại và cố gắng serialize chúng với System.Text.Json. Tiếp theo, tôi chơi trò “đánh Whack-a-mole” với các lỗi một lúc, và cuối cùng thừa nhận rằng mình không thể làm cho nó hoạt động. Cuối cùng, chúng ta sẽ xem xét quá trình deserialization.

Bài viết này được viết với .NET 11 preview 7. Tôi hy vọng rằng nhiều edge case này sẽ được giải quyết trước khi .NET 11 phát hành chính thức, nhưng tôi sẽ không ngạc nhiên nếu hỗ trợ đầy đủ hơn sẽ được đẩy sang .NET 12, vì vậy hãy ghi nhớ điều đó. Và nếu tôi chỉ bỏ lỡ điều gì đó hiển nhiên, vui lòng cho tôi biết trong phần bình luận!

Các loại union trong C# 15 với từ khóa union

Unions là một trong những cấu trúc dữ liệu cơ bản được sử dụng liên tục trong thế giới lập trình chức năng; chúng có trong F#, TypeScript, Rust… gần như bất kỳ ngôn ngữ chức năng nào. Có nhiều loại union khác nhau, nhưng về cơ bản chúng cho phép có một loại duy nhất có thể đại diện cho hai thứ khác nhau.

Ví dụ, hãy tưởng tượng chúng ta có ba loại record khác nhau, chứa các thuộc tính khác nhau, đại diện cho Hệ điều hành:

public record Windows(string Version);
public record Linux(string Distro, string Version);
public record MacOS(string Name, int Version);

Lưu ý rằng các loại này không cần có bất kỳ giá trị chung nào. Trước C# 15, không có tùy chọn tuyệt vời để đại diện cho thứ có thể là đối tượng Windows hoặc Linux hoặc MacOS, nhưng trong C# 15, chúng ta có thể sử dụng từ khóa union, như hiển thị bên dưới:

//     👇 Sử dụng `union` làm type
public union SupportedOS(Windows, Linux, MacOS);
//             👇 Liệt kê các type là thành phần của union

Sau đó, bạn có thể tạo một instance của loại SupportedOS theo một vài cách:

// Bạn có thể gọi new và truyền vào một instance
SupportedOS os = new SupportedOS(new MacOS("Tahoe", 25));

// Hoặc bạn có thể sử dụng conversion ngầm định (sẽ gọi new() phía sau)
SupportedOS os = new MacOS("Tahoe", 25);

Cách thông thường để làm việc với unions là sử dụng biểu thức switch để pattern match dựa trên loại, và vì chúng là toàn diện, bạn không cần bao gồm trường hợp discard _ =>:

string GetDescription(SupportedOS os) => os switch
{
    Windows windows => $"Windows {windows.Version}",
    Linux linux => $"{linux.Distro} {linux.Version}",
    MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})",
}; // lưu ý: không cần discard _

Unions có thể được tạo từ các loại tùy chỉnh, như trong ví dụ trước, nhưng bạn cũng có thể tạo union cho các loại bạn không sở hữu, ví dụ:

public union IntOrString(int, string);

Để đọc thêm về unions, xem bài viết trước của tôi trong đó tôi đi sâu vào cách chúng được triển khai và cách tối ưu hóa chúng. Hiện tại, chúng ta sẽ chuyển sang closed class hierarchies.

“Closed class hierarchy” là gì?

Một closed class hierarchy là một lớp kế thừa lớp chỉ có thể được định nghĩa trong một assembly duy nhất. Việc cố gắng kế thừa từ một lớp đóng trong một assembly khác sẽ là lỗi biên dịch. Điều này dễ thấy nhất khi hoạt động.

Hãy tưởng tượng bạn có các lớp sau, tất cả trong cùng một assembly:

// Tạo một lớp cơ sở đóng
public closed class Pet
{
    public string Name { get; set; }
}

// Mỗi lớp kế thừa từ lớp Pet đóng
internal class Dog : Pet
{
    public string FavouriteToy { get; set; } 
}

internal sealed class Cat : Pet
{
    public string FavouriteFood { get; set; }
}

Các lớp Dog và Cat đều kế thừa từ Pet. Đây là tất cả kiến thức C# cơ bản; từ khóa closed thực sự không thay đổi bất cứ điều gì về điều đó.

Lưu ý rằng khi bạn khai báo một loại là closed, nó thực sự cũng biến nó thành lớp abstract.

Sự khác biệt là nếu chúng ta tạo một assembly mới và cố gắng kế thừa từ Pet trong một assembly khác:

// Assembly 2
public class Horse : Pet { }

thì nó sẽ không biên dịch! Thay vào đó, bạn sẽ nhận được lỗi như sau:

error CS9382: 'Horse': cannot use a closed type 'Pet' from another assembly as a base type.

Điều này có thể rất hữu ích để mô hình hóa một số lĩnh vực nhất định và có thể đơn giản hóa logic trong mã của bạn đáng kể. Nó cũng có nghĩa là trình biên dịch có thể áp dụng nhiều “bảo đảm toàn diện” tương tự như đối với unions, (mặc dù về mặt khái niệm chúng khá khác nhau – không có “hierarchy” trong unions).

Điều đó cho chúng ta nền tảng cần thiết, vì vậy bây giờ chúng ta sẽ xem cách hoạt động với serialize và deserialize System.Text.Json.

Serialize unions và closed hierarchies với System.Text.Json

Trong phần này, tôi sẽ đơn giản tạo một số dữ liệu chứa sự kết hợp của unions và closed hierarchies, và sau đó chúng ta sẽ cố gắng serialize dữ liệu đó với System.Text.Json, và chúng ta sẽ xử lý hậu quả.

Định nghĩa cấu trúc dữ liệu

Đối với unions, chúng ta sẽ sử dụng hai ví dụ tôi đã hiển thị trước đó:

// Union int/string cơ bản
public union IntOrString(int, string);

// Các loại POCO record đơn giản
public record Windows(string Version);
public record Linux(string Distro, string Version);
public record MacOS(string Name, int Version);

// Union của POCOs
public union SupportedOS(Windows, Linux, MacOS);

Đối với closed class hierarchy, tôi muốn sử dụng thứ gì đó phức tạp hơn một chút so với ví dụ tôi đã đưa ra ở trên, vì vậy trong ví dụ này chúng ta có hai lớp closed classes:

Pet (đóng)
 |___ Cat
 |___ Dog
      |___ Labrador
      |___ Collie

Vì vậy, chúng ta có một loại abstract đóng (Pet), một loại “cơ sở” Dog, và ba “loại lá” (Cat, Labrador, Collie). Ngoài ra, tôi đã tạo Pet là kiểu public và phần còn lại là internal (một trong những cách sử dụng tôi thấy cho closed class hierarchies là mô hình hóa các API thư viện tương tự như vậy):

public closed class Pet
{
    public string Name { get; set; }
}

internal class Dog : Pet
{
    public string FavouriteToy { get; set; } 
}

internal sealed class Labrador : Dog
{
    public bool Hungry { get; } = true;
}

internal sealed class Collie : Dog
{
    public bool Nervous { get; } = true;
}

internal sealed class Cat : Pet
{
    public string FavouriteFood { get; set; }
}

Cuối cùng, chúng ta có loại mà chúng ta thực sự sẽ serialize, đó chỉ là một trong mỗi hierarchy ở trên:

public class Data
{
    public IntOrString Value { get; set; }
    public SupportedOS Os { get; set; }
    public Pet Pet { get; set; }
}

Thử nghiệm serialize mặc định

Bây giờ chúng ta đã có cấu trúc dữ liệu, chúng ta sẽ tạo một số dữ liệu và cố gắng serialize nó. Đối tượng data bên dưới chứa 42 trong union IntOrString, Windows trong union SupportedOS, và một Labrador trong closed class hierarchy Pet:

var data = new Data
{
    Value = new IntOrString(42);
    Os = new(new Windows("11")),
    Pet = new Labrador { Name = "Goose", FavouriteToy = "Ducky" }
};

Sau đó, chúng ta sẽ thực hiện serialize cơ bản nhất, sử dụng các thiết lập serializer mặc định:

string serialized = JsonSerializer.Serialize(data);
Console.WriteLine(serialized);

Điều này cho kết quả sau (đã định dạng lại một chút để dễ đọc hơn)

{
  "Value": 42,
  "Os": {"Version":"11"},
  "Pet": {"Name":"Goose"}
}

Điều này đã cho thấy một số điều thú vị:

  • Instance IntOrString trong "Value" đã được serialize trực tiếp dưới dạng số 42, và không phải là một sub-object như bạn có thể mong đợi, tức là nó không được serialize dưới dạng "Value" : { "Value" : 42 }.
  • Thuộc tính "OS" được serialize dưới dạng sub object, điều này có lẽ bạn sẽ mong đợi.
  • Thuộc tính "Pet" không bao gồm các thuộc tính con của Labrador, chỉ có thuộc tính "Name" được khai báo trên lớp Pet.

Hành vi serialize IntOrString khá thú vị, vì nó xử lý loại union như một “primitive” thực sự bậc cao trong trường hợp này, điều này có thể hữu ích nếu bạn đang tương tác với các JSON API nơi một thuộc tính thực sự có thể là int hoặc string.

Tuy nhiên, serialize Pet có lẽ không phải là điều chúng ta muốn, vì vậy chúng ta sẽ khắc phục điều đó trước.

Cố gắng sửa serialize closed class hierarchy

Hiện tại, chúng ta đang mất rất nhiều thông tin khi serialize instance Labrador trong thuộc tính "Pet", vì vậy chúng ta sẽ cố gắng khắc phục điều đó. Đây về cơ bản là “serialize polymorphism bình thường” với System.Text.Json, có nghĩa là bạn mong đợi cần thêm [JsonDerivedType] và chú thích mọi thứ đúng cách, liệt kê tất cả các loại derived có thể.

InferClosedTypePolymorphism tuyệt vời… đôi khi

Tuy nhiên, đây là một hierarchy đóng, vì vậy tất cả các loại derived có thể đều được biết tại thời điểm biên dịch. Vì vậy trong .NET 11, có một tùy chọn mới trên JsonSerializerOptions (và trong RC1, cũng trên thuộc tính [JsonPolymorphic]) sẽ tự xây dựng hierarchy đó, không cần phải rõ ràng, sử dụng InferClosedTypePolymorphism! 🎉

var opts = new JsonSerializerOptions
{
    InferClosedTypePolymorphism = true, // Bật suy luận derived types cho closed classes
};

string serialized = JsonSerializer.Serialize(data, opts); // 👈 Sử dụng tùy chọn
Console.WriteLine(serialized);

Thật không may, khi chúng ta chạy mã này, chúng ta nhận được ngoại lệ:

System.InvalidOperationException: The inferred derived type 'Dog' is 
less accessible than the polymorphic base type 'Pet'. Inferred derived
 types must be at least as accessible as the base type.

Hmm. Vì vậy, việc tôi tạo các loại derived có thể truy cập thấp hơn (internal) so với cơ sở (public) rõ ràng là không được hỗ trợ. Điều đó có vẻ hơi ngạc nhiên đối với tôi, đặc biệt vì đây là loại mẫu mà tôi có thể thấy khá hữu ích cho closed hierarchies nói chung. Nhưng rõ ràng chúng ta phải tạo mọi thứ là public hoặc internal để serializer hài lòng.

Tuy nhiên, sau khi thay đổi mọi thứ thành public và chạy lại, chúng ta gặp ngoại lệ khác:

System.NotSupportedException: Runtime type 'Labrador' is not supported
 by polymorphic type 'Pet'. Path: $.Pet.

OK… không chắc điều đó có ý nghĩa gì, nhưng có thể là vì Dog không phải là loại closed? Điều đó có vẻ hợp lý, vì tính năng này đang suy luận về các loại đóng, và Labrador không phải là một phần nghiêm ngặt của điều đó. Vì vậy, hãy đánh dấu Dog cũng là loại closed và thử lại.

Và vâng, một ngoại lệ khác:

System.InvalidOperationException: Specified type 'Dog' is not a supported
derived type for the polymorphic type 'Pet'. Derived types must not be open
generic type definitions, must be assignable to the base type and cannot be
abstract classes or interfaces unless 'JsonUnknownDerivedTypeHandling.
FallBackToNearestAncestor' is specified.

Vì điều này rõ ràng nói rằng một derived type không thể là lớp abstract (cách các loại closed được triển khai), điều đó dường như ngụ ý rằng việc có “hai” lớp hierarchy bên dưới Pet, và để InferClosedTypePolymorphism tự động phát hiện nó đơn giản là không hoạt động.

Điều đó dường như là một hạn chế lớn, nhưng tôi đã thử nhiều biến thể khác nhau để cố gắng tìm điều gì đó hoạt động, và tôi đã thất bại.

Tôi nghi ngờ nó liên quan đến thực tế là mặc dù tôi đã sealed các loại derived Labrador và Collie trong trường hợp này, không có gì nói rằng bạn phải làm như vậy, và sau đó tổng thể hierarchy không được biết, chỉ có các con trực tiếp của loại closed được đảm bảo toàn diện (trừ khi bạn seal các con).

CHỈNH SỬA: Tôi đã tạo issue để hỏi về hành vi này, và có vẻ như hành vi này sẽ được hỗ trợ khi .NET 11 phát hành chính thức! Vì vậy, hãy coi các giải pháp tạm thời sau chỉ là vấn đề tạm thời 🎉

Cuối cùng, tôi chỉ tìm thấy hai tùy chọn:

  • Chỉ có một derived type duy nhất bên dưới lớp closed.
  • Chỉ định rõ ràng các derived types.

Để ý mục đích chính của InferClosedTypePolymorphism là không phải làm điều thứ hai đó, thật đáng tiếc, nhưng cuối cùng đó là những gì tôi đã thực hiện. Điều đó cho phép tôi khôi phục lại các derived type internal của mình. Đây là hierarchy cuối cùng tôi đã kết thúc:

// Tôi cảm thấy những cái này _nên_ được InferClosedTypePolymorphism suy luận, nhưng nó không hoạt động
[JsonDerivedType(typeof(Collie), typeDiscriminator: nameof(Collie))]
[JsonDerivedType(typeof(Labrador), typeDiscriminator: nameof(Labrador))]
[JsonDerivedType(typeof(Cat), typeDiscriminator: nameof(Cat))]
public closed class Pet
{
    public string Name { get; set; }
}

//       👇 Đã tạo closed để đầy đủ
internal closed class Dog : Pet
{
    public string FavouriteToy { get; set; } 
}

// Collie, Labrador, Cat được định nghĩa như trước

Với các thuộc tính đó, chúng ta có thể quay lại serialize cơ bản của mình:

string serialized = JsonSerializer.Serialize(data, opts);
Console.WriteLine(serialized);

Và bây giờ chúng ta có loại Labrador được serialize đầy đủ, với discriminator $type được bao gồm:

{
  "Value":42,
  "Os": {"Version":"11"},
  "Pet": {"$type":"Labrador", "FavouriteToy":"Ducky", "Hungry":true, "Name":"Goose"}
}

Đã đến lúc chuyển sang phần deserialization.

Deserialize unions và closed hierarchies với System.Text.Json

Bắt đầu từ dữ liệu đã serialize ở trên, chúng ta sẽ bắt đầu bằng việc thực hiện deserialize đơn giản:

var serialized = """{"Value":42,"Os":{"Version":"11"},"Pet":{"$type":"Labrador","Hungry":true,"FavouriteToy":"Ducky","Name":"Goose"}}""";

var result = JsonSerializer.Deserialize<Data>(serialized);

Ồ không, ngoại lệ khác:

System.Text.Json.JsonException: JSON value type 'Object' is ambiguous for
union type 'SupportedOS' because multiple case types can use this value type.
Specify a custom type classifier to support deserialization.
Path: $.Os | LineNumber: 0 | BytePositionInLine: 18.

Vậy chuyện gì đang xảy ra ở đây?

Hiểu hạn chế deserialize union

Union SupportedOS của chúng ta đã thất bại trong việc deserialize, mặc dù có vẻ như union IntOrString đã deserialize thành công. Điều đó là do các quyết định cơ bản được đưa ra khi lần đầu thêm hỗ trợ cho unions vào System.Text.Json, nơi chỉ các hình dạng union có thể được xác định một cách rõ ràng từ loại của giá trị JSON hỗ trợ serialize và deserialize tự động.

Vì vậy, những điều sau đây có thể được deserialize tự động, vì mỗi loại trong union serialize dưới dạng thứ gì đó khác nhau trong JSON:

  • IntOrString(int, string) serialize dưới dạng Number và String tương ứng.
  • DoubleOrGuid(double, Guid) cũng serialize dưới dạng Number và String.
  • BoolOrDictionary(bool, Dictionary) serialize dưới dạng Bool và Object.
  • Many(decimal, DateTime, bool, Dog) serialize dưới dạng Number, String, Bool và Object!

Ngược lại, những điều sau đây không hoạt động, vì chúng serialize dưới dạng cùng một loại đối tượng JSON:

  • Num(int, long)—cả hai trường hợp đều là Number.
  • When(DateTime, DateTimeOffset)—cả hai trường hợp đều là String.
  • Pet(Cat, Dog)—cả hai trường hợp đều là Object.

Và dựa trên các quy tắc trên, tất cả các trường hợp trong SupportedOS đều được serialize dưới dạng Object:

public union SupportedOS(Windows, Linux, MacOS);

Điều này giải thích tại sao deserialize bị hỏng, trong khi IntOrString thì không. Cá nhân tôi, tôi thật sự không thích việc unions không round-trip theo mặc định. Tôi thích chúng mặc định “xấu” hơn (ví dụ: sử dụng tham số $type) thay vì cách tiếp cận hiện tại.

Có rất nhiều thảo luận về hành vi mặc định này trong đánh giá API, với nhiều người tranh luận cùng quan điểm với tôi. Cuối cùng, có nhiều mối quan tâm hơn về serialize “đẹp” cho trường hợp primitives, để tương tác với các API TypeScript. Tôi có thể hiểu điều đó, nhưng tôi cảm thấy rằng serialize mặc định chỉ có thể hỗ trợ một tập con rất nhỏ các API đó, nên đây là mặc định sai. Tôi cũng ước có một cách đơn giản để chỉ cần bật phương pháp discriminator $type, nhưng tôi không thể tìm thấy.

Sửa deserialize union với custom factory

Trong trường hợp bạn đã lướt qua phần trước, tl;dr là trừ khi bạn có unions rất đơn giản, nơi các loại có thể được xác định đơn giản dựa trên loại của JSON token, bạn sẽ gặp khó khăn.

Tôi ước họ đã cung cấp tùy chọn để chỉ sử dụng phương pháp $type discriminator “bình thường” cho unions, nhưng họ dường như tập trung vào kịch bản typescript/API bên thứ ba (và deserialization cấu trúc) nên họ về cơ bản đã bỏ qua kịch bản round-trip.

Hiện tại, tôi thực sự không thể tìm ra cách nào để ép buộc $type discriminator vào đường dẫn serialize/deserialize 🙁 Điều đó chắc chắn có thể là do bất lực của tôi, vì vậy vui lòng cho tôi biết nếu có cách dễ dàng. Tôi hy vọng thêm [JsonPolymorphic] sẽ đủ, nhưng nó không hợp pháp để áp dụng cho union.

Thay vào đó, hook duy nhất tôi có thể tìm ra là triển khai một JsonTypeClassifierFactory tùy chỉnh đọc đối tượng vào bộ nhớ, và sau đó để bạn xác định loại nào nó đại diện. Liệu điều đó có khả thi hay không sẽ phụ thuộc vào “hình dạng” của JSON đã serialize của bạn, nhưng miễn là có một thuộc tính “duy nhất” cho mỗi trường hợp, thì bạn có thể làm được.

Sau đây là ví dụ về classifier cho loại SupportedOS, dựa vào Distro và Name là duy nhất trong các trường hợp. Nếu không, chúng ta tìm thuộc tính Version, nếu không, chúng ta thất bại. Nó không cảm thấy tuyệt vời, nhưng đây là ví dụ về “phối hợp cấu trúc” bạn bị buộc phải thực hiện do thiếu discriminator rõ ràng:

public class SupportedOSClassifier : JsonTypeClassifierFactory<SupportedOS>
{
    public override JsonTypeClassifier CreateJsonClassifier(
        JsonTypeClassifierContext context, JsonSerializerOptions options)
    {
        // Utf8JsonReader đã đọc và cache JSON trong bộ nhớ
        return static (ref Utf8JsonReader reader) =>
        {
            // Tất cả các case types đều là Objects
            if (reader.TokenType is not JsonTokenType.StartObject)
            {
                return null;
            }

            bool sawVersion = false;

            // Đọc các thuộc tính cho đến khi kết thúc đối tượng
            while (reader.Read() && reader.TokenType is not JsonTokenType.EndObject)
            {
                // Hai thuộc tính này là duy nhất cho các case types
                if (reader.ValueTextEquals("Distro"u8)) return typeof(Linux);
                if (reader.ValueTextEquals("Name"u8)) return typeof(MacOS);

                // Version có trên _tất cả_ các case, nên không phân biệt
                if (reader.ValueTextEquals("Version"u8))
                {
                    // mọi thứ đều có version, nên không phải discriminator, nhưng cần thiết
                    sawVersion = true;
                }

                reader.Read(); // Chuyển đến giá trị của thuộc tính.
                reader.Skip(); // Bỏ qua giá trị.
            }

            // Nếu chúng ta có thuộc tính Version, thì chỉ còn lại một case
            // nếu không, thì chúng ta đã thất bại trong việc khớp bất kỳ điều gì, vì vậy thất bại
            return sawVersion ? typeof(Windows) : null;
        };
    }
}

Tất cả những gì còn lại là áp dụng điều đó cho union với thuộc tính [JsonUnion] mới:

[JsonUnion(TypeClassifier = typeof(SupportedOsClassifier))]
public union SupportedOS(Windows, Linux, MacOS);

Và với điều đó, deserialize của chúng ta bây giờ hoạt động như mong đợi 🎉

JsonUnionTypeStructuralClassifier sẽ có trong RC1

Đáng lưu ý rằng có một phiên bản tổng quát của classifier “cấu trúc” mà tôi đã viết ở trên sẽ có trong RC1, gọi là JsonUnionTypeStructuralClassifier. Nó đã được hợp nhất, nhưng vì RC1 chưa phát hành, tôi chưa thử nghiệm.

Tuy nhiên, dựa trên các unit tests, có vẻ như nó cũng sẽ có nhiều hạn chế, ví dụ:

  • Nó sẽ không hoạt động nếu bất kỳ trường hợp union nào của bạn là chính union.
  • Nó sẽ không hoạt động nếu bạn không có thuộc tính duy nhất (trừ khi được đánh dấu required, xem bên dưới).
  • Nó sẽ không hoạt động nếu bạn có các đối tượng POCO và không-POCO hỗn hợp (ví dụ: public union ObjectOrDictionaryUnion(Point, Dictionary<string, int>)).

Nó có thể sẽ lấp đầy khoảng trống do hỗ trợ union mặc định để lại, đến mức tôi nghi ngờ hầu hết mọi người sẽ kết thúc bằng cách gắn nó vào tất cả các unions của họ. Điều bực bội là đánh giá API đó đã chỉ ra rằng điều này sẽ có hiệu suất tồi tệ, điều này khiến tôi càng bực bội hơn khi chúng ta không thể chỉ có discriminator $type thay vì 😅

Tuy nhiên, tôi không nghĩ rằng JsonUnionTypeStructuralClassifier sẽ hoạt động trên union SupportedOS của tôi trừ khi chúng ta thực hiện một vài thay đổi, bằng cách đánh dấu các thuộc tính khác nhau là required:

public record Windows([property: JsonRequired] string Version);
public record Linux([property: JsonRequired] string Distro, string Version);
public record MacOS([property: JsonRequired] string Name, int Version);

Bằng cách đánh dấu các thuộc tính này là required, tôi nghĩ rằng điều đó có nghĩa là JsonUnionTypeStructuralClassifier sẽ có thể deserialize từng trường hợp, nhưng tôi sẽ phải thử nghiệm chắc chắn khi RC1 phát hành.

Nhìn chung, tin tốt là System.Text.Json sẽ hỗ trợ unions và closed hierarchies trong .NET 11, ngay cả khi không có gì thực sự hoạt động theo cách tôi muốn 😅

Tóm tắt

Trong bài viết này, tôi đã cung cấp giới thiệu ngắn gọn về unions và closed hierarchies và cung cấp kịch bản kiểm tra đơn giản cho serialize. Sau đó, tôi đã serialize dữ liệu với System.Text.Json và gặp vấn đề với closed hierarchy. Tôi đã cố gắng sử dụng InferClosedTypePolymorphism nhưng cuối cùng phát hiện rằng nó không hoạt động với hierarchy mà tôi đang kiểm tra, và phải resorts lại chú thích thủ công với [JsonDerivedType].

Khi deserialize, tôi gặp vấn đề với các tùy chọn mặc định được chọn cho deserialize union, cụ thể là nó chỉ hoạt động out-of-the-box nếu mỗi loại union case serialize thành một primitive JSON khác nhau, tức là Number/String/Object. Nếu bạn có hai đối tượng—ví dụ: Pet(Cat, Dog) kinh điển—thì deserialize thất bại, và bạn phải viết classifier thủ công. Tôi đã hiển thị cách viết classifier như vậy, và có phiên bản tổng quát sẽ phát hành trong RC1, nhưng nhìn chung điều đó có vẻ như một cái bẫy mà nhiều người sẽ rơi vào.

Sê-ri này: Khám phá .NET 11 preview

© 2026 Andrew Lock | .NET Escapades. Tất cả quyền được bảo lưu.