diff --git a/docs/core/whats-new/dotnet-11/libraries.md b/docs/core/whats-new/dotnet-11/libraries.md index 9087ba9509389..d1561d5f89d02 100644 --- a/docs/core/whats-new/dotnet-11/libraries.md +++ b/docs/core/whats-new/dotnet-11/libraries.md @@ -175,96 +175,158 @@ These methods provide both high-level convenience methods (that allocate and ret ### System.Text.Json improvements -#### Generic type info retrieval +#### Union serialization -A common pattern when working with `System.Text.Json` type metadata is to retrieve a from . -Previously, you had to manually downcast from the non-generic method. -New generic and methods return strongly typed metadata directly, eliminating the cast. +**C# union types.** Starting in .NET 11, C# offers union types. For example, `Reading` can hold an `int` or a `string`: -:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonTypeInfoGeneric"::: +:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonUnionType"::: -This is particularly useful when working with source generation, NativeAOT, and polymorphic serialization scenarios where type metadata access is common. +A `Reading` that holds `"hello"` round-trips as a JSON string: -#### Naming and ignore defaults +:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonUnionSerialization"::: -The naming and ignore options available in now include: +The serializer writes the active case directly, without a wrapper or `$type` discriminator. The JSON output depends on the active case: -- ****: A new built-in naming policy that converts property names to PascalCase. It joins the existing camelCase, snake_case, and kebab-case policies. -- **Per-member naming policy**: The new attribute lets you override the naming policy on individual properties or fields, giving you fine-grained control without a custom converter. -- **Type-level ignore conditions**: Applying at the class or struct level sets the default ignore behavior for all members, so you no longer need to repeat the attribute on every nullable property. +| Union state | JSON output | +| - | - | +| `string` case with value `"hello"` | `"hello"` | +| `int` case with value `42` | `42` | +| Default struct union with no active case | `null` | -:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonNamingIgnore"::: +By default, deserialization selects the case from the JSON token kind: number, string, Boolean, array, or object. Two object cases share a token kind, so the default classifier can't distinguish them. To select an object case by its root-level property names, use with the built-in : + +:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonUnionStructuralType"::: + +:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonUnionStructuralClassifier"::: + +The payload's `Lives` property selects `Cat`. The structural classifier narrows the candidate cases by root-level property names and required properties; it doesn't inspect property values or nested content. If the payload matches zero or multiple cases, deserialization throws . The classifier doesn't support multiple non-object cases with the same JSON token kind, nested unions, polymorphic cases, or . -#### F# discriminated union support +For other case-selection rules, derive from . The factory receives a describing the union cases and creates a delegate that reads JSON and selects a case type. Register the factory on one union through , on an options instance through , or on a source-generated context through . -The serializer now understands F# discriminated unions out of the box. Apps that share types between F# producers and C# consumers no longer need a custom converter for the most common shapes: +If a union declares separate `T` and `T?` cases, the serializer selects the nullable case for a `null` payload. A non-null payload is ambiguous without a custom classifier because both cases use the same JSON shape. If no case accepts `null`, JSON `null` deserializes to the default union value, which also serializes as `null`. + +For language syntax, see [C# 15 union types](../../../csharp/whats-new/csharp-15.md#union-types). For more about classifiers and advanced contract metadata such as and , see [Serialize union types](../../../standard/serialization/system-text-json/union-types.md). + +**F# discriminated unions.** The serializer represents cases without fields as JSON strings and cases with fields as JSON objects that contain a `$type` discriminator: ```fsharp type Shape = + | Point | Circle of radius: float - | Square of side: float - -let json = System.Text.Json.JsonSerializer.Serialize(Circle 1.5) -// {"$type":"Circle","radius":1.5} + | Rectangle of height: float * length: float ``` -#### Utf8JsonWriter.Reset with options +| F# value | JSON output | +| - | - | +| `Point` | `"Point"` | +| `Circle 3.14` | `{"$type":"Circle","radius":3.14}` | +| `Rectangle(10.0, 20.0)` | `{"$type":"Rectangle","height":10,"length":20}` | - now accepts a parameter, so writer instances can be repooled with different options without allocating a new writer: +For cases without fields, deserialization accepts both `"Point"` and `{"$type":"Point"}`. Class, struct, and recursive unions are supported. applies to case and field names, while on a case takes precedence. You can also change the discriminator property through . -:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="Utf8JsonWriterReset"::: +> [!IMPORTANT] +> F# discriminated-union support uses reflection only. It doesn't support `System.Text.Json` source generation, trimming, or Native AOT. -#### SerializeAsyncEnumerable improvements +#### JSON Lines output - gains two new capabilities: +Four overloads serialize an to either a or . For each output type, one overload accepts and one accepts . -- **`PipeWriter` target:** For pipeline-based I/O scenarios, new overloads accept a as the output destination, making it easy to integrate JSON streaming directly. -- **`topLevelValues` parameter:** For NDJSON output, set this parameter to `true` to write each item as a top-level JSON value separated by newlines instead of wrapping all items in a JSON array. Both `Stream` and `PipeWriter` overloads support the parameter. +With the default `topLevelValues: false`, the output remains one JSON array. Set `topLevelValues: true` to write canonical JSON Lines (JSONL). The serializer writes each value compactly and follows it with a line feed (LF), `\n`, including the final value. JSONL output uses LF regardless of and ignores so that each value occupies one line. :::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonSerializeAsyncEnumerablePipe"::: -#### Numeric type and binary-schema support +#### Polymorphic hierarchy support + +For a C# `closed` hierarchy, the serializer can infer the derived types, including generic specializations, and assign deterministic discriminators. Configure inference at any of these scopes: + +```csharp +[JsonPolymorphic(InferClosedTypePolymorphism = true)] +closed record Shape; +record Circle(double Radius) : Shape; +record Square(double Length) : Shape; +``` -`System.Text.Json` includes built-in converters for , , , and . These converters work with both the reflection-based serializer and the source generator, including named floating-point literals when enabled through . +- Set for reflection-based serialization or as a global runtime setting. +- Set so the source generator creates the required metadata at compile time. +- Set to `true` on one hierarchy. Set it to `false` to opt that hierarchy out of a global setting. -:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonNumericTypes"::: +Explicit registrations take precedence and replace the inferred list instead of extending it. If source-generated metadata doesn't contain inferred or explicitly registered derived types, enabling only the runtime option fails fast. A per-type opt-in on a type that isn't `closed` throws with reflection and produces a source-generation error. Combining a per-type opt-in with explicit derived-type registrations produces a source-generation warning because the explicit registrations replace inference. - also identifies the base64 representation used for `byte[]`, , and . For `byte[]`, the exported schema now includes a `contentEncoding` keyword: +Open generic derived-type registrations are also supported. For example, one attribute registers `Derived` for every compatible closed `Base`: -```diff -- { "type": ["string", "null"] } -+ { "type": ["string", "null"], "contentEncoding": "base64" } +```csharp +[JsonDerivedType(typeof(Derived<>), "derived")] +public class Base { } +public class Derived : Base { } ``` -The `Memory` and `ReadOnlyMemory` schemas remain non-nullable (`"type": "string"`) and also include `contentEncoding`. +The resolver supports these type-argument patterns: -#### C# union type serialization +| Pattern | Example resolution | +| - | - | +| Direct binding | `Base` resolves `Derived : Base` to `Derived`. | +| Reordered parameters | `Base` resolves `Derived : Base` to `Derived`. | +| Partially concrete parameters | `Base` resolves `Derived : Base` to `Derived`. | +| Nested or array parameters | `Base>` resolves `Derived : Base>` to `Derived`. | -`System.Text.Json` can now serialize and deserialize C# union types. The serializer recognizes a union through the new `JsonTypeInfoKind.Union` contract kind, reads and writes the active case, and supports both the reflection-based serializer and the source generator. When you serialize a union, `System.Text.Json` writes the value of whichever case is active, so a union of `int` and `string` round-trips cleanly: +The same rules apply to generic interfaces and types nested in generic outer types. The registration must resolve to exactly one closed derived type and satisfy its generic constraints. A ground-type mismatch, an unbound parameter, a constraint violation, an ambiguous match, or an incompatible shape throws with reflection. For source generation, an unresolved registration produces the [SYSLIB1229](../../../fundamentals/syslib-diagnostics/syslib1220-1229.md) warning, and the generated hierarchy still fails when the serializer configures it. Suppressing the warning doesn't make the registration valid. -```json -{ - "id": 1, - "payload": "hello" -} -``` +For serializer configuration guidance, see [Infer polymorphism from a closed hierarchy](../../../standard/serialization/system-text-json/polymorphism.md#infer-polymorphism-from-a-closed-hierarchy) and [Configure open generic derived types](../../../standard/serialization/system-text-json/polymorphism.md#configure-open-generic-derived-types). For more information about the language feature, see [C# 15 closed hierarchies](../../../csharp/whats-new/csharp-15.md#closed-hierarchies). -```json -{ - "id": 2, - "payload": 42 -} +#### Source-generation and contract metadata + +The `System.Text.Json` source generator supports more contract shapes: + +- Members marked with can have `private`, `internal`, or `protected` accessors. Private, internal, and protected fields are also supported when they're marked with `[JsonInclude]`. +- An inaccessible constructor marked with can participate in deserialization. +- An omitted `init`-only property retains its property-initializer value. The generated contract sets the property after construction only when the JSON payload contains that property. + +`JsonSerializer` also deserializes types with constructors whose parameters use `in`, `ref`, `out`, or `ref readonly`. The serializer binds `in`, `ref`, and `ref readonly` parameters by their element type. An `out` parameter doesn't bind a JSON constructor argument; the constructor receives an initialized output location, and a matching writable property can receive the JSON value after construction. + +For more information, see [Source-generation modes in System.Text.Json](../../../standard/serialization/system-text-json/source-generation-modes.md) and [Use immutable types and properties](../../../standard/serialization/system-text-json/immutability.md). + +Generic and methods return without a manual cast. `GetTypeInfo()` throws if the options can't resolve the type, while `TryGetTypeInfo()` returns `false` in that case. Resolver and configuration exceptions can still propagate. These methods simplify metadata access in source-generation, Native AOT, and polymorphic serialization scenarios. For more information, see [Get strongly typed metadata](../../../standard/serialization/system-text-json/custom-contracts.md#get-strongly-typed-metadata). + +:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonTypeInfoGeneric"::: + +#### Property names and ignore conditions + + and provide built-in PascalCase conversion. Apply to a class, struct, interface, property, or field to select a naming policy at the type or member level. + +Naming precedence, from highest to lowest, is , a member-level `[JsonNamingPolicy]`, a type-level `[JsonNamingPolicy]`, , and the original member name. + + can now set a default ignore condition on classes, structs, and interfaces. A member-level `[JsonIgnore]` takes precedence over the type-level condition, which takes precedence over . isn't valid at the type level; reflection throws , while source generation reports `SYSLIB1226` and ignores the type-level attribute. + +:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonNamingIgnore"::: + +For more information, see [Apply a naming policy to a type or member](../../../standard/serialization/system-text-json/customize-properties.md#apply-a-naming-policy-to-a-type-or-member) and [Ignore properties based on a type-level condition](../../../standard/serialization/system-text-json/ignore-properties.md#ignore-properties-based-on-a-type-level-condition). + +#### Converters and collection contracts + +**Numeric converters.** The serializer includes built-in converters for , , , and . The converters support number-handling options, dictionary keys, and JSON Schema export. For generated metadata, exposes , , , and . + +**Open generic converters.** can reference an open generic converter on a generic type or member: + +```csharp +[JsonConverter(typeof(OptionConverter<>))] +public readonly struct Option { } ``` -The new `JsonUnionAttribute` and `JsonUnionCaseInfo` APIs, along with type-classifier APIs (`JsonTypeClassifier` and `JsonSerializerOptions.TypeClassifiers`), let you customize how cases are discovered and named. Union types are a C# language preview feature. For more information, see [What's new in C# 15](../../../csharp/whats-new/csharp-15.md#union-types). +The converter's total generic arity must match the target type's arity. The serializer closes the converter with the target type arguments without requiring . For supported nesting patterns, constraints, and error behavior, see [Use open generic converters with `JsonConverter`](../../../standard/serialization/system-text-json/converters-how-to.md#use-open-generic-converters-with-jsonconverter). -For a union with object-shaped cases, the built-in `JsonUnionTypeStructuralClassifier` selects the active case from its distinguishing property names, so you don't need to author a custom classifier: +**Extension data.** A member marked with can use with `string` keys and either `object` or values. During deserialization, the serializer assigns a mutable implementation. If the property already contains entries, the serializer copies them first and then adds JSON properties, so an incoming value replaces an existing value with the same key. A extension-data member writes its child properties directly into the containing object. For example, an `Id` property and a `JsonObject` entry named `nested` produce `{"Id":1,"nested":true}`. For more information, see [Handle overflow JSON](../../../standard/serialization/system-text-json/handle-overflow.md#handle-overflow-json). -:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="JsonUnionStructuralClassifier"::: +**Read-only sets.** serializes as a JSON array and deserializes it into a instance. For source-generated metadata, creates the collection contract. + +For more information about built-in numeric and collection contracts, see [Supported types in System.Text.Json](../../../standard/serialization/system-text-json/supported-types.md). -#### Closed-hierarchy polymorphism inference +#### Writer reuse - adds so the serializer can infer polymorphic metadata for C# closed hierarchies without requiring explicit annotations on each base type. Explicit registrations still take precedence. + accepts a together with either a or . After you flush one JSON document, you can reuse or pool the writer with a different output target and different options instead of constructing another writer instance: + +:::code language="csharp" source="./snippets/csharp/Libraries.cs" id="Utf8JsonWriterReset"::: + +For more information, see [Reuse a writer](../../../standard/serialization/system-text-json/use-utf8jsonwriter.md#reuse-a-writer). You can also opt a single closed hierarchy in to this behavior with , without changing the application-wide `JsonSerializerOptions` setting: @@ -481,7 +543,6 @@ On Windows, `Process` now uses overlapped I/O for redirected stdout/stderr, whic - [BitArray span constructors](#bitarray-span-constructors) - [BitArray.PopCount](#bitarraypopcount) -- [IReadOnlySet support in JSON serialization](#ireadonlyset-support-in-json-serialization) - [EqualityComparer\.Create](#equalitycomparertcreate) #### BitArray span constructors @@ -496,10 +557,6 @@ The span-based constructors avoid the intermediate array allocation that `new Bi The class now includes a method that returns the number of bits set to `true` in the array. This provides an efficient way to count set bits without manually iterating through the array. -#### IReadOnlySet support in JSON serialization - -The class now includes a method, enabling JSON serialization support for collections. - #### EqualityComparer\.Create To create an equality comparer from a key selector function, use the new factory method. You can pass an optional for the key type itself: diff --git a/docs/core/whats-new/dotnet-11/overview.md b/docs/core/whats-new/dotnet-11/overview.md index 0d8a073b47a76..a1dc7c8fa242a 100644 --- a/docs/core/whats-new/dotnet-11/overview.md +++ b/docs/core/whats-new/dotnet-11/overview.md @@ -37,7 +37,7 @@ The .NET 11 libraries include new APIs for: - expansion with run-and-capture helpers, fire-and-forget launches, lifecycle methods, tighter handle control, new for suspended starts, for safe process lookup, and with for signaling processes and inspecting how they exited. - Compression, including improved Base64 APIs, new methods for ZIP archive entries, Zstandard compression in , CRC32 validation when reading ZIP entries, and a `Reset()` method on the streamless Deflate, ZLib, and GZip encoders and decoders. - New numeric APIs, including IEEE 754 decimal floating-point types (, , and ), for delimiter-aware parsing, and generic . -- System.Text.Json improvements, including generic type info retrieval, , per-member naming policy overrides, type-level ignore conditions, F# discriminated union support, with options, `SerializeAsyncEnumerable` overloads for `PipeWriter` targets and top-level values (NDJSON) output, serialization of C# union types with the new `JsonUnionTypeStructuralClassifier`, built-in converters for `BFloat16` and the new decimal floating-point types, and base64 schema metadata from `JsonSchemaExporter`. +- System.Text.Json improvements, including C# and F# union support, JSON Lines (JSONL) output, expanded polymorphism and source generation, new naming and ignore controls, and built-in numeric converters and collection contracts. - Built-in OpenTelemetry metrics for . - Discriminated-union scaffolding (`UnionAttribute` and `IUnion`) in . - Tar archive format selection and GNU sparse format 1.0 support. diff --git a/docs/core/whats-new/dotnet-11/snippets/csharp/Libraries.cs b/docs/core/whats-new/dotnet-11/snippets/csharp/Libraries.cs index 43eef5796ec08..7293e1cd2697d 100644 --- a/docs/core/whats-new/dotnet-11/snippets/csharp/Libraries.cs +++ b/docs/core/whats-new/dotnet-11/snippets/csharp/Libraries.cs @@ -103,7 +103,7 @@ static void JsonTypeInfoExample() { // JsonSerializerOptions options = new(JsonSerializerDefaults.Web); - options.MakeReadOnly(); + options.MakeReadOnly(populateMissingResolver: true); // Before: manual downcast required JsonTypeInfo info1 = (JsonTypeInfo)options.GetTypeInfo(typeof(MyRecord)); @@ -124,6 +124,7 @@ static void JsonNamingIgnoreExample() { // // Type-level JsonIgnore: all members use WhenWritingNull by default + // Type-level JsonNamingPolicy: ReleaseVersion uses snake_case // Per-member JsonNamingPolicy: EventName uses camelCase even though the // serializer options use PascalCase var options = new JsonSerializerOptions @@ -131,10 +132,10 @@ static void JsonNamingIgnoreExample() PropertyNamingPolicy = JsonNamingPolicy.PascalCase }; - var data = new EventData { EventName = "Launch", Notes = null }; + var data = new EventData { EventName = "Launch", ReleaseVersion = "11", Notes = null }; string json = JsonSerializer.Serialize(data, options); Console.WriteLine(json); - // {"eventName":"Launch"} -- Notes omitted (null), EventName camel-cased + // {"eventName":"Launch","release_version":"11"} -- Notes omitted (null), EventName camel-cased // } @@ -309,18 +310,24 @@ static async IAsyncEnumerable GenerateNumbers() } } - var pipe = new Pipe(); + using var arrayStream = new MemoryStream(); + PipeWriter arrayPipe = PipeWriter.Create(arrayStream); // Write a JSON array: [0,1,2,3,4] await JsonSerializer.SerializeAsyncEnumerable( - pipe.Writer, + arrayPipe, GenerateNumbers()); + await arrayPipe.CompleteAsync(); - // Write NDJSON (one value per line): 0\n1\n2\n3\n4\n + using var jsonlStream = new MemoryStream(); + PipeWriter jsonlPipe = PipeWriter.Create(jsonlStream); + + // Write JSON Lines (one value per line): 0\n1\n2\n3\n4\n await JsonSerializer.SerializeAsyncEnumerable( - pipe.Writer, + jsonlPipe, GenerateNumbers(), topLevelValues: true); + await jsonlPipe.CompleteAsync(); // } @@ -336,6 +343,17 @@ static void JsonNumericTypesExample() // } + static void JsonUnionSerializationExample() + { + // + Reading reading = new("hello"); + string json = JsonSerializer.Serialize(reading); + Reading copy = JsonSerializer.Deserialize(json); + Console.WriteLine(json); // "hello" + Console.WriteLine(copy.Value); // hello + // + } + static void JsonUnionStructuralClassifierExample() { // @@ -399,23 +417,32 @@ static void NullableUnderlyingTypeExample() record MyRecord(string Name, int Value); +[JsonNamingPolicy(JsonKnownNamingPolicy.SnakeCaseLower)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] sealed class EventData { [JsonNamingPolicy(JsonKnownNamingPolicy.CamelCase)] public string EventName { get; set; } = ""; + public string ReleaseVersion { get; set; } = ""; + public string? Notes { get; set; } } readonly record struct Measurement(Decimal64 Voltage); +// +public union Reading(int, string); +// + +// [JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))] public union PetUnion(Dog, Cat); public sealed record Dog(string Name, string Breed); public sealed record Cat(string Name, int Lives); +// [JsonSerializable(typeof(PetUnion))] internal partial class PetJsonContext : JsonSerializerContext; diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index fe707ee890ac6..49da3ab34e3b8 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -609,6 +609,8 @@ items: href: ../standard/serialization/system-text-json/preserve-references.md - name: Serialize polymorphic types href: ../standard/serialization/system-text-json/polymorphism.md + - name: Serialize union types + href: ../standard/serialization/system-text-json/union-types.md - name: Use extension methods on HttpClient href: ../standard/serialization/system-text-json/httpclient-extensions.md - name: Read/write JSON without using JsonSerializer diff --git a/docs/standard/serialization/system-text-json/converters-how-to.md b/docs/standard/serialization/system-text-json/converters-how-to.md index 30b851854a00f..5aa0f254bb6dd 100644 --- a/docs/standard/serialization/system-text-json/converters-how-to.md +++ b/docs/standard/serialization/system-text-json/converters-how-to.md @@ -1,7 +1,7 @@ --- title: "How to write custom converters for JSON serialization - .NET" description: "Learn how to create custom converters for the JSON serialization classes that are provided in the System.Text.Json namespace." -ms.date: 03/23/2026 +ms.date: 08/18/2026 no-loc: [System.Text.Json, Newtonsoft.Json] helpviewer_keywords: - "JSON serialization" @@ -89,11 +89,13 @@ The `Enum` type is similar to an open generic type: a converter for `Enum` has t ## Use open generic converters with [JsonConverter] -Starting in .NET 11, supports open generic converter types on generic types when the total type parameter arity matches. This feature lets you apply a `[JsonConverter]` attribute directly using an open generic converter type (for example, `typeof(OptionConverter<>)`) without implementing a . The serializer automatically constructs the closed generic converter at runtime. +Starting in .NET 11, supports open generic converter types on generic types when the total type parameter arity matches. This feature lets you apply a `[JsonConverter]` attribute directly using an open generic converter type (for example, `typeof(OptionConverter<>)`) without implementing a . The serializer automatically constructs the closed generic converter. + +Unlike the reflection-based factory example above, the source generator resolves the closed converter type at compile time. You can use this pattern with source generation and Native AOT if the converter itself uses AOT-compatible APIs and the generated context provides metadata for the types it handles. The `OptionConverter` example uses `options.GetTypeInfo()` to get metadata for its inner value. ### Define the generic type -Annotate your generic type with `[JsonConverter]`, specifying the open generic converter type. The type parameter count on the converter must match the target type: +Annotate your generic type with `[JsonConverter]`, specifying the open generic converter type. The converter and target type must have matching total generic arity: :::code language="csharp" source="snippets/converters-how-to/csharp/OpenGenericConverter.cs" id="OptionType"::: @@ -149,7 +151,7 @@ Continue to use when: * You register the converter through instead of the `[JsonConverter]` attribute. > [!NOTE] -> If the type parameter count on the converter doesn't match the target type, an is thrown at runtime. +> At runtime, using an open generic converter on a non-generic type or with mismatched total generic arity throws an . The message identifies the converter and target type. ## The use of `Utf8JsonReader` in the `Read` method diff --git a/docs/standard/serialization/system-text-json/custom-contracts.md b/docs/standard/serialization/system-text-json/custom-contracts.md index 1d57b718b6397..6605bc80c4e46 100644 --- a/docs/standard/serialization/system-text-json/custom-contracts.md +++ b/docs/standard/serialization/system-text-json/custom-contracts.md @@ -1,7 +1,8 @@ --- title: Custom serialization and deserialization contracts description: "Learn how to write your own contract resolution logic to customize the JSON contract for a type." -ms.date: 06/15/2023 +ms.date: 08/18/2026 +ai-usage: ai-assisted --- # Customize a JSON contract @@ -45,15 +46,30 @@ There are two ways to plug into customization. Both involve obtaining a resolver - If a type isn't handled, should return `null` for that type. - You can also combine your custom resolver with others, for example, the default resolver. The resolvers will be queried in order until a non-null value is returned for the type. +## Get strongly typed metadata + +Starting in .NET 11, use and as strongly typed alternatives to casting the result of : + +```csharp +JsonTypeInfo typeInfo = + options.GetTypeInfo(); + +bool found = options.TryGetTypeInfo( + out JsonTypeInfo? optionalTypeInfo); +``` + +`TryGetTypeInfo` returns `false` when no resolver supplies metadata for `T`. + ## Configurable aspects -The property indicates how the converter serializes a given type—for example, as an object or as an array, and whether its properties are serialized. You can query this property to determine which aspects of a type's JSON contract you can configure. There are four different kinds: +The property indicates how the converter serializes a given type—for example, as an object or as an array, and whether its properties are serialized. Query this property to determine which aspects of a type's JSON contract you can configure. The property has five possible values: | `JsonTypeInfo.Kind` | Description | |---------------------|-------------| | | The converter will serialize the type into a JSON object and uses its properties. **This kind is used for most class and struct types and allows for the most flexibility.** | | | The converter will serialize the type into a JSON array. This kind is used for types like `List` and array. | | | The converter will serialize the type into a JSON object. This kind is used for types like `Dictionary`. | +| | The converter serializes the active case value from a union. Starting in .NET 11, this kind is used for C# union types and exposes case, classifier, constructor, and deconstructor metadata. | | | The converter doesn't specify how it will serialize the type or what `JsonTypeInfo` properties it will use. This kind is used for types like , `int`, and `string`, and for all types that use a custom converter. | ## Modifiers @@ -68,6 +84,7 @@ The following table shows the modifications you can make and how to achieve them | Add or remove properties | `JsonTypeInfoKind.Object` | Add or remove items from the list. | [Serialize private fields](#example-serialize-private-fields) | | Conditionally serialize a property | `JsonTypeInfoKind.Object` | Modify the predicate for the property. | [Ignore properties with a specific type](#example-ignore-properties-with-a-specific-type) | | Customize number handling for a specific type | `JsonTypeInfoKind.None` | Modify the value for the type. | [Allow int values to be strings](#example-allow-int-values-to-be-strings) | +| Customize union cases or classification | `JsonTypeInfoKind.Union` | Modify the union cases, classifier, constructor, or deconstructor on . | [Serialize union types](union-types.md#customize-a-union-contract) | ## Example: Increment a property's value diff --git a/docs/standard/serialization/system-text-json/customize-properties.md b/docs/standard/serialization/system-text-json/customize-properties.md index 86d53e9cf177e..65067a537fe49 100644 --- a/docs/standard/serialization/system-text-json/customize-properties.md +++ b/docs/standard/serialization/system-text-json/customize-properties.md @@ -1,7 +1,7 @@ --- title: How to customize property names and values with System.Text.Json description: "Learn how to customize property names and values when serializing with System.Text.Json in .NET." -ms.date: 05/06/2025 +ms.date: 08/18/2026 no-loc: [System.Text.Json, Newtonsoft.Json] dev_langs: - "csharp" @@ -12,6 +12,7 @@ helpviewer_keywords: - "serialization" - "objects, serializing" ms.topic: how-to +ai-usage: ai-assisted --- # How to customize property names and values with System.Text.Json @@ -19,7 +20,8 @@ ms.topic: how-to By default, property names and dictionary keys are unchanged in the JSON output, including case. Enum values are represented as numbers. And properties are serialized in the order they're defined. However, you can customize these behaviors by: - Specifying specific serialized property and enum member names. -- Using a built-in [naming policy](xref:System.Text.Json.JsonNamingPolicy), such as camelCase, snake_case, or kebab-case, for property names and dictionary keys. +- Using a built-in [naming policy](xref:System.Text.Json.JsonNamingPolicy), such as camelCase, PascalCase, snake_case, or kebab-case, for property names and dictionary keys. +- Applying a naming policy to a type or member. - Using a custom naming policy for property names and dictionary keys. - Serializing enum values as strings, with or without a naming policy. - Configuring the order of serialized properties. @@ -60,6 +62,7 @@ The following table shows the built-in naming policies and how they affect prope | Naming policy | Description | Original property name | Converted property name | |-----------------------------------------------------------|-|-----------------------|-------------------------| | | First word starts with a lower case character.
Successive words start with an uppercase character. | `TempCelsius` | `tempCelsius` | +| \*\* | First word starts with an uppercase character.
Successive words start with an uppercase character. | `tempCelsius` | `TempCelsius` | | \* | Words are separated by hyphens.
All characters are lowercase. | `TempCelsius` | `temp-celsius` | | \* | Words are separated by hyphens.
All characters are uppercase. | `TempCelsius` | `TEMP-CELSIUS` | | \* | Words are separated by underscores.
All characters are lowercase. | `TempCelsius` | `temp_celsius` | @@ -67,6 +70,8 @@ The following table shows the built-in naming policies and how they affect prope \* Available in .NET 8 and later versions. +\*\* Available in .NET 11 and later versions. + The following example shows how to use camel case for all JSON property names by setting to : :::code language="csharp" source="snippets/how-to/csharp/RoundTripCamelCasePropertyNames.cs" id="Serialize"::: @@ -94,6 +99,43 @@ The naming policy: > [!NOTE] > None of the built-in naming policies support letters that are surrogate pairs. For more information, see [dotnet/runtime issue 90352](https://github.com/dotnet/runtime/issues/90352). +## Apply a naming policy to a type or member + +Starting in .NET 11, apply to a class, struct, interface, property, or field. Pass a value to select a built-in policy. A type-level attribute sets the naming policy for the type's properties and fields. A member-level attribute sets the policy for one property or field. + +The following example sets a type-level policy and overrides it for one property: + +```csharp +[JsonNamingPolicy(JsonKnownNamingPolicy.CamelCase)] +public class WeatherForecast +{ + public int TemperatureCelsius { get; set; } + [JsonNamingPolicy(JsonKnownNamingPolicy.SnakeCaseLower)] public string? SummaryText { get; set; } +} +``` + +```vb + +Public Class WeatherForecast + Public Property TemperatureCelsius As Integer + Public Property SummaryText As String +End Class +``` + +For `TemperatureCelsius = 25` and `SummaryText = "Hot"`, the resulting JSON is `{"temperatureCelsius":25,"summary_text":"Hot"}`. + +The serializer selects a JSON property name in this order, from highest to lowest precedence: + +- A on the property or field. +- A member-level . +- A type-level . +- . +- The original member name. + +The protected constructor lets a derived attribute supply a custom . Reflection-based serialization evaluates the custom policy at run time. + +Source generation can't execute a custom policy at compile time. For affected members, it uses the original CLR name and doesn't apply the global . + ## Use a custom JSON property naming policy To use a custom JSON property naming policy, create a class that derives from and override the method, as shown in the following example: diff --git a/docs/standard/serialization/system-text-json/extract-schema.md b/docs/standard/serialization/system-text-json/extract-schema.md index addf82e24d349..f6986de12a26b 100644 --- a/docs/standard/serialization/system-text-json/extract-schema.md +++ b/docs/standard/serialization/system-text-json/extract-schema.md @@ -1,7 +1,8 @@ --- title: JSON schema exporter description: Learn how to use the JsonSchemaExporter class to extract JSON schema documents from .NET types. -ms.date: 10/15/2024 +ms.date: 09/24/2026 +ai-usage: ai-assisted dev_langs: - "csharp" --- @@ -16,6 +17,12 @@ The following code snippet shows an example. As can be seen in this example, the exporter distinguishes between nullable and non-nullable properties, and it populates the `required` keyword by virtue of a constructor parameter being optional or not. +Starting in .NET 11, the exporter recognizes the , , , and types. It exports schemas for their nullable forms and for named literals when you enable . + +## Schemas for union types + +Starting in .NET 11, describes a C# [union](union-types.md) with an untagged `anyOf` schema that has a branch for each case. The union adds no discriminator because writes only the active case. By contrast, when you [enable inference for a closed hierarchy](polymorphism.md#infer-polymorphism-from-a-closed-hierarchy), JSON serialized as the closed base type includes a `$type` discriminator that identifies the derived type. The `anyOf` described here is `JsonSchemaExporter` output; ASP.NET Core generates OpenAPI documents separately. + ## Configure the schema output You can influence the schema output by configuration specified in the or instance that you call the method on. The following example sets the naming policy to , writes numbers as strings, and disallows unmapped properties. diff --git a/docs/standard/serialization/system-text-json/handle-overflow.md b/docs/standard/serialization/system-text-json/handle-overflow.md index 57843048cbe1f..0d25d568feb67 100644 --- a/docs/standard/serialization/system-text-json/handle-overflow.md +++ b/docs/standard/serialization/system-text-json/handle-overflow.md @@ -1,7 +1,7 @@ --- title: How to handle overflow JSON or use JsonElement or JsonNode in System.Text.Json description: "Learn how to handle overflow JSON or use JsonElement or JsonNode while using System.Text.Json to serialize and deserialize JSON in .NET." -ms.date: 07/21/2021 +ms.date: 08/18/2026 no-loc: [System.Text.Json, Newtonsoft.Json] dev_langs: - "csharp" @@ -12,6 +12,7 @@ helpviewer_keywords: - "serialization" - "objects, serializing" ms.topic: how-to +ai-usage: ai-assisted --- # How to handle overflow JSON or use JsonElement or JsonNode @@ -44,11 +45,21 @@ And the JSON to be deserialized is this: } ``` -If you deserialize the JSON shown into the type shown, the `DatesAvailable` and `SummaryWords` properties have nowhere to go and are lost. To capture extra data such as these properties, apply the [[JsonExtensionData]](xref:System.Text.Json.Serialization.JsonExtensionDataAttribute) attribute to a property of type `Dictionary` or `Dictionary`: +If you deserialize the JSON shown into the type shown, the `DatesAvailable` and `SummaryWords` properties have nowhere to go and are lost. To capture extra data such as these properties, apply the [[JsonExtensionData]](xref:System.Text.Json.Serialization.JsonExtensionDataAttribute) attribute to a property or field. Use one of these supported declarations: + +* Declare the extension-data member as . +* Declare the extension-data member as [`IDictionary`](xref:System.Collections.Generic.IDictionary`2). +* Declare the extension-data member as [`IDictionary`](xref:System.Collections.Generic.IDictionary`2). +* Declare the extension-data member as [`IReadOnlyDictionary`](xref:System.Collections.Generic.IReadOnlyDictionary`2) in .NET 11 and later. +* Declare the extension-data member as [`IReadOnlyDictionary`](xref:System.Collections.Generic.IReadOnlyDictionary`2) in .NET 11 and later. + +For mutable dictionary extension data, use any type assignable to one of the supported `IDictionary` interfaces, such as `Dictionary`. The read-only declarations must use one of the exact `IReadOnlyDictionary` interface types listed above. :::code language="csharp" source="snippets/how-to/csharp/WeatherForecast.cs" id="WFWithExtensionData"::: :::code language="vb" source="snippets/how-to/vb/WeatherForecast.vb" id="WFWithExtensionData"::: +For an `IReadOnlyDictionary` extension-data member, .NET 11 materializes a `Dictionary` or `Dictionary`. The member must be writable because the serializer seeds the new dictionary from existing values and assigns it back. If an incoming JSON property duplicates an existing key, the incoming value wins. + The following table shows the result of deserializing the JSON shown earlier into this sample type. The extra data becomes key-value pairs of the `ExtensionData` property: | Property | Value | Notes | @@ -80,6 +91,8 @@ When the target object is serialized, the extension data key value pairs become Notice that the `ExtensionData` property name doesn't appear in the JSON. This behavior lets the JSON make a round trip without losing any extra data that otherwise wouldn't be deserialized. +Starting in .NET 11, serialization flattens `JsonObject` extension data into the containing JSON object. The `JsonObject` properties appear directly in the containing object, not under the extension-data member name. + The following example shows a round trip from JSON to a deserialized object and back to JSON: :::code language="csharp" source="snippets/how-to/csharp/RoundtripExtensionData.cs" highlight="11-12"::: diff --git a/docs/standard/serialization/system-text-json/ignore-properties.md b/docs/standard/serialization/system-text-json/ignore-properties.md index 783815bd4baf0..1d36065fe14c5 100644 --- a/docs/standard/serialization/system-text-json/ignore-properties.md +++ b/docs/standard/serialization/system-text-json/ignore-properties.md @@ -1,7 +1,7 @@ --- title: How to ignore properties with System.Text.Json description: "Learn how to ignore properties when serializing with System.Text.Json in .NET." -ms.date: 10/22/2025 +ms.date: 08/18/2026 ms.custom: devdivchpfy22 no-loc: [System.Text.Json, Newtonsoft.Json] dev_langs: @@ -21,6 +21,7 @@ ai-usage: ai-assisted When serializing C# objects to JavaScript Object Notation (JSON), by default, all public properties are serialized. If you don't want some of them to appear in the resulting JSON, you have several options. In this article, you learn how to ignore properties based on various criteria: * [Individual properties](#ignore-individual-properties) +* [Properties based on a type-level condition](#ignore-properties-based-on-a-type-level-condition) * [All read-only properties](#ignore-all-read-only-properties) * [All null-value properties](#ignore-all-null-value-properties) * [All default-value properties](#ignore-all-default-value-properties) @@ -53,6 +54,35 @@ The following example illustrates the use of the [[JsonIgnore]](xref:System.Text :::code language="csharp" source="snippets/how-to-contd/csharp/JsonIgnoreAttributeExample.cs" highlight="8,11,14"::: :::code language="vb" source="snippets/how-to-contd/vb/JsonIgnoreAttributeExample.vb" ::: +## Ignore properties based on a type-level condition + +Starting in .NET 11, apply `[JsonIgnore(Condition = ...)]` to a class, struct, or interface to set the default ignore condition for its properties and fields: + +```csharp +[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] +public class Forecast +{ + public string? Summary { get; set; } +} +``` + +```vb + +Public Class Forecast + Public Property Summary As String +End Class +``` + +The serializer applies ignore settings in this order, from highest to lowest precedence: + +* A member-level . +* A type-level . +* . + +At the type level, is invalid. Reflection-based serialization throws an , and source generation reports [SYSLIB1226](../../../fundamentals/syslib-diagnostics/syslib1220-1229.md). Because `Always` is the default condition, specify `Condition` when you apply `[JsonIgnore]` to a type. + +A type-level condition doesn't ignore non-nullable value-type members. The type-level condition still overrides the global `DefaultIgnoreCondition`, so those members remain in the JSON even when the global condition is `WhenWritingDefault`. + ## Ignore all read-only properties A property is read-only if it contains a public getter but not a public setter. To ignore all read-only properties when serializing, set the to `true`, as shown in the following example: diff --git a/docs/standard/serialization/system-text-json/immutability.md b/docs/standard/serialization/system-text-json/immutability.md index 480aa6b16981f..9c03e1532732a 100644 --- a/docs/standard/serialization/system-text-json/immutability.md +++ b/docs/standard/serialization/system-text-json/immutability.md @@ -1,7 +1,8 @@ --- title: Use immutable types and properties description: "Learn how to deserialize JSON to immutable types and properties in .NET." -ms.date: 10/20/2023 +ms.date: 08/18/2026 +ai-usage: ai-assisted no-loc: [System.Text.Json, Newtonsoft.Json] dev_langs: - "csharp" @@ -27,6 +28,8 @@ By default, `System.Text.Json` uses the default public parameterless constructor In .NET 7 and earlier versions, the `[JsonConstructor]` attribute can only be used with public constructors. +In .NET 8 and later versions, reflection mode supports non-public constructors marked with `[JsonConstructor]`. Starting in .NET 11, source-generation mode supports them too. + The parameter names of a parameterized constructor must match the property names and types. Matching is case-insensitive, and the constructor parameter must match the actual property name even if you use [[JsonPropertyName]](xref:System.Text.Json.Serialization.JsonPropertyNameAttribute) to rename a property. In the following example, the name for the `TemperatureC` property is changed to `celsius` in the JSON, but the constructor parameter is still named `temperatureC`: :::code language="csharp" source="snippets/how-to-contd/csharp/ImmutableTypesCtorParms.cs" highlight="9,13-15"::: @@ -38,6 +41,33 @@ Besides `[JsonPropertyName]`, the following attributes support deserialization w - [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute) - [[JsonNumberHandling]](xref:System.Text.Json.Serialization.JsonNumberHandlingAttribute) +## By-reference constructor parameters + +Starting in .NET 11, `JsonSerializer` deserializes types whose constructor parameters use the `in`, `ref`, `out`, and `ref readonly` modifiers. + +| Parameter modifier | Deserialization behavior | +|--------------------|--------------------------| +| `in`, `ref`, and `ref readonly` | The serializer binds each parameter by name and uses its underlying element type for type matching. | +| `out` | The serializer doesn't bind the parameter to JSON. It discards the value that the constructor assigns. | + +In the following constructor, the serializer binds `temperatureC` from JSON. It doesn't bind `isValid`: + +```csharp +public Forecast(in int temperatureC, out bool isValid) +{ + TemperatureC = temperatureC; + isValid = true; +} +``` + +In Visual Basic, a `ByRef` constructor parameter follows the `ref` behavior shown in the table: + +```vb +Public Sub New(ByRef temperatureC As Integer) + TemperatureC = temperatureC +End Sub +``` + ## Records Records are also supported for both serialization and deserialization, as shown in the following example: @@ -57,8 +87,14 @@ By including a property with a private setter, you can still deserialize that pr In .NET 8 and later versions, you can also use the [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute) attribute to opt non-public *members* into the serialization contract for a given type. +Starting in .NET 11, source generation supports `private`, `internal`, and `protected` members that you mark with `[JsonInclude]`. It also supports `private`, `internal`, and `protected` accessors on properties that you mark with `[JsonInclude]`. Source generation also supports inaccessible constructors marked with [[JsonConstructor]](xref:System.Text.Json.Serialization.JsonConstructorAttribute). + > [!NOTE] -> In source-generation mode, you can't serialize `private` members or use `private` accessors by annotating them with the [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute) attribute. And you can only serialize `internal` members or use `internal` accessors if they're in the same assembly as the generated . +> In .NET 10 and earlier versions, source generation doesn't support `private` or `protected` members or accessors. Applying the [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute) attribute to the member or property doesn't remove this limitation. Source generation supports `internal` members and accessors only when they're in the same assembly as the generated . It doesn't support inaccessible constructors, even when you mark them with `[JsonConstructor]`. + +## Init-only properties + +`System.Text.Json` deserializes `init`-only properties like any other settable property. Starting in .NET 11, a source-generated setter runs only when the JSON payload contains the property. An omitted property retains its initializer value. ## Read-only properties diff --git a/docs/standard/serialization/system-text-json/polymorphism.md b/docs/standard/serialization/system-text-json/polymorphism.md index bbec94bd7bb44..c8f40c0cee98a 100644 --- a/docs/standard/serialization/system-text-json/polymorphism.md +++ b/docs/standard/serialization/system-text-json/polymorphism.md @@ -1,7 +1,7 @@ --- title: How to serialize properties of derived classes with System.Text.Json description: "Learn how to serialize polymorphic objects while serializing to and deserializing from JSON in .NET." -ms.date: 10/18/2024 +ms.date: 08/18/2026 no-loc: [System.Text.Json, Newtonsoft.Json] dev_langs: - "csharp" @@ -12,6 +12,7 @@ helpviewer_keywords: - "serialization" - "objects, serializing" ms.topic: how-to +ai-usage: ai-assisted --- # How to serialize properties of derived classes with System.Text.Json @@ -560,6 +561,65 @@ JsonSerializer.Serialize(new BasePointWithTimeSeries()); JsonSerializer.Serialize(Of IPoint)(New BasePointWithTimeSeries()) ``` +## Infer polymorphism from a closed hierarchy + +Starting in .NET 11, `System.Text.Json` can infer derived types from a [C# 15 closed hierarchy](../../../csharp/language-reference/keywords/closed.md). Enable inference on one hierarchy through : + +```csharp +[JsonPolymorphic(InferClosedTypePolymorphism = true)] +public closed class Shape { } +public sealed class Circle : Shape { } +public sealed class Square : Shape { } +``` + +The serializer registers each derived type and uses its simple type name as a string discriminator. For example, a `Circle` payload contains `"$type":"Circle"`. + +To enable inference for every closed hierarchy that an options instance handles, set : + +```csharp +var options = new JsonSerializerOptions +{ + InferClosedTypePolymorphism = true +}; +``` + +For source generation, set : + +```csharp +[JsonSourceGenerationOptions(InferClosedTypePolymorphism = true)] +[JsonSerializable(typeof(Shape))] +internal partial class AppJsonContext : JsonSerializerContext; +``` + +Set inference on the source-generation attribute when you use a generated context. Enabling only the runtime `JsonSerializerOptions` property can't add derived-type metadata that the source generator didn't emit. + +The type-level attribute takes precedence over the global setting. Set `InferClosedTypePolymorphism = false` on to opt one hierarchy out of a global setting. + +Explicit registrations replace inference for that hierarchy. They don't extend the inferred list. Applying `InferClosedTypePolymorphism = true` to a type that isn't `closed` throws an with reflection-based serialization and produces a source-generation error. Inferred types with duplicate simple names produce discriminator collisions, and every inferred type must be at least as accessible as the closed base. + +## Configure open generic derived types + +Starting in .NET 11, accepts an open generic derived type when the serializer can resolve a unique closed type from the serialized base type: + +```csharp +[JsonDerivedType(typeof(Derived<>), "derived")] +public class Base; +public class Derived : Base; +``` + +```vb + +Public Class Base(Of T) +End Class +Public Class Derived(Of T) + Inherits Base(Of T) +End Class +``` + +For `Base`, the serializer registers `Derived`. The same resolution supports generic interfaces, reordered type parameters, nested generic arguments, arrays, and derived types that fix some base type arguments to concrete types. + +Every derived type parameter must be inferable from the closed base type, the substitution must be unambiguous, and the resulting type must satisfy its generic constraints. Reflection-based serialization throws for an unsupported specialization. Source generation reports [SYSLIB1229](../../../fundamentals/syslib-diagnostics/syslib1220-1229.md), and the generated hierarchy still fails when the serializer configures it. Suppressing the warning doesn't make the registration valid. + ## Configure polymorphism with the contract model For use cases where attribute annotations are impractical or impossible (such as large domain models, cross-assembly hierarchies, or hierarchies in third-party dependencies), to configure polymorphism use the [contract model](custom-contracts.md). The contract model is a set of APIs that can be used to configure polymorphism in a type hierarchy by creating a custom subclass that dynamically provides polymorphic configuration per type, as shown in the following example: @@ -633,3 +693,4 @@ End Class * [System.Text.Json overview](overview.md) * [How to serialize and deserialize JSON](how-to.md) +* [Serialize union types](union-types.md) diff --git a/docs/standard/serialization/system-text-json/reflection-vs-source-generation.md b/docs/standard/serialization/system-text-json/reflection-vs-source-generation.md index db4212f2d0b1e..aded4d5abf8ce 100644 --- a/docs/standard/serialization/system-text-json/reflection-vs-source-generation.md +++ b/docs/standard/serialization/system-text-json/reflection-vs-source-generation.md @@ -1,7 +1,8 @@ --- title: How to choose reflection or source generation in System.Text.Json description: "Learn how to choose reflection or source generation in System.Text.Json." -ms.date: 10/30/2023 +ms.date: 08/18/2026 +ai-usage: ai-assisted no-loc: [System.Text.Json] --- @@ -42,6 +43,9 @@ Source generation can be used in two modes: Source generation for `System.Text.Json` requires C# 9.0 or a later version. +> [!NOTE] +> F# discriminated union support works only in reflection mode. It requires dynamic code and untrimmed reflection metadata. You can't use it with source generation or Native AOT. For more information, see [F# discriminated unions](supported-types.md#f-discriminated-unions). + ## Feature comparison Choose reflection or source-generation modes based on the following benefits that each one offers: @@ -50,7 +54,7 @@ Choose reflection or source-generation modes based on the following benefits tha |------------------------------------------------------|------------|---------------------|----------------------------| | Simpler to code. | ✔️ | ❌ | ❌ | | Simpler to debug. | ❌ | ✔️ | ✔️ | -| Supports non-public members. | ✔️ | ✔️* | ✔️* | +| Supports `[JsonInclude]` on non-public members. | ✔️ | ✔️* | ✔️* | | Supports all available serialization customizations. | ✔️ | ❌† | ❌† | | Reduces start-up time. | ❌ | ✔️ | ✔️ | | Reduces private memory usage. | ❌ | ✔️ | ✔️ | @@ -58,5 +62,5 @@ Choose reflection or source-generation modes based on the following benefits tha | Facilitates trim-safe app size reduction. | ❌ | ✔️ | ✔️ | | Increases serialization throughput. | ❌ | ❌ | ✔️ | -\* The source generator supports *some* non-public members, for example, internal types in the same assembly. -† Source-generated contracts can be modified using the contract customization API. +\* Starting in .NET 11, source generation supports `private`, `internal`, and `protected` members that you explicitly mark with [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute). It also supports `private`, `internal`, and `protected` accessors on properties that you mark with `[JsonInclude]`. Metadata-based source generation supports inaccessible constructors that you mark with [[JsonConstructor]](xref:System.Text.Json.Serialization.JsonConstructorAttribute). Generated setters run only for `init`-only properties that appear in the JSON, so omitted properties keep their initializer values. In .NET 10 and earlier versions, source generation doesn't support `private` or `protected` members or accessors, or inaccessible constructors. The generated context can access `internal` members and accessors only when they share an assembly. For more information, see [Non-public members and constructors](source-generation-modes.md#non-public-members-and-constructors). +† Use the contract customization API to modify source-generated contracts. diff --git a/docs/standard/serialization/system-text-json/snippets/converters-how-to/csharp/OpenGenericConverter.cs b/docs/standard/serialization/system-text-json/snippets/converters-how-to/csharp/OpenGenericConverter.cs index 07b49000843aa..8a04ed538ac40 100644 --- a/docs/standard/serialization/system-text-json/snippets/converters-how-to/csharp/OpenGenericConverter.cs +++ b/docs/standard/serialization/system-text-json/snippets/converters-how-to/csharp/OpenGenericConverter.cs @@ -29,7 +29,7 @@ public override Option Read( return default; } - T value = JsonSerializer.Deserialize(ref reader, options)!; + T value = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo())!; return new Option(value); } @@ -42,7 +42,7 @@ public override void Write( return; } - JsonSerializer.Serialize(writer, value.Value, options); + JsonSerializer.Serialize(writer, value.Value, options.GetTypeInfo()); } } // diff --git a/docs/standard/serialization/system-text-json/source-generation-modes.md b/docs/standard/serialization/system-text-json/source-generation-modes.md index 13e4fb5cc4c86..75d5b5b21a35c 100644 --- a/docs/standard/serialization/system-text-json/source-generation-modes.md +++ b/docs/standard/serialization/system-text-json/source-generation-modes.md @@ -1,7 +1,8 @@ --- title: Source-generation modes in System.Text.Json description: Learn about the two different source-generation modes in System.Text.Json. -ms.date: 02/21/2025 +ms.date: 08/18/2026 +ai-usage: ai-assisted no-loc: [System.Text.Json] helpviewer_keywords: - "JSON serialization" @@ -22,9 +23,23 @@ You can use source generation to move the metadata collection process from runti The performance improvements provided by source generation can be substantial. For example, [test results](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/#how-source-generation-provides-benefits) have shown up to 40% or more startup time reduction, private memory reduction, throughput speed increase (in serialization optimization mode), and app size reduction. -### Known issues +### Non-public members and constructors + +By default, both reflection mode and source-generation mode include only `public` properties and fields in the serialization contract. + +Starting in .NET 11, source generation supports members that you explicitly mark with the [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute) attribute. The member can be `private`, `internal`, or `protected`. It also supports `private`, `internal`, and `protected` accessors on properties that you mark with `[JsonInclude]`. Source generation also supports inaccessible constructors marked with [[JsonConstructor]](xref:System.Text.Json.Serialization.JsonConstructorAttribute). + +On .NET 11, the generated accessors use . -Only `public` properties and fields are supported by default in either serialization mode (reflection or source-generation). However, reflection mode supports the use of `private` members, while source-generation mode doesn't. For example, if you apply the [JsonInclude attribute](xref:System.Text.Json.Serialization.JsonIncludeAttribute) to a `private` property or a property that has a `private` setter or getter, it will be serialized in reflection mode. Source-generation mode supports only `public` or `internal` members and `public` or `internal` accessors of `public` properties. If you set `[JsonInclude]` on `private` members or accessors and choose source-generation mode, a `NotSupportedException` will be thrown at runtime. +A source-generated setter for an `init`-only property runs only when the JSON payload contains that property. An `init`-only property that the payload omits keeps the value from its property initializer. + +In .NET 10 and earlier versions, source generation has the following limitations: + +* Source generation doesn't support `private` or `protected` members or accessors. If you mark such a member with `[JsonInclude]`, the serializer throws a at runtime. +* Source generation supports `internal` members and accessors only when they're accessible to the generated in the same assembly. +* Source generation doesn't support constructors that are inaccessible to the generated context, even when you mark them with `[JsonConstructor]`. + +### Known issues For information about other known issues with source generation, see the [GitHub issues that are labeled "source-generator"](https://github.com/dotnet/runtime/issues?q=is%3Aopen+is%3Aissue+label%3Aarea-System.Text.Json+label%3Asource-generator) in the *dotnet/runtime* repository. diff --git a/docs/standard/serialization/system-text-json/supported-types.md b/docs/standard/serialization/system-text-json/supported-types.md index e33d40a228418..aa333c95fc931 100644 --- a/docs/standard/serialization/system-text-json/supported-types.md +++ b/docs/standard/serialization/system-text-json/supported-types.md @@ -1,7 +1,8 @@ --- title: "Supported types in System.Text.Json" description: "Learn which types are supported for serialization by the APIs in the System.Text.Json namespace." -ms.date: 11/25/2024 +ms.date: 08/18/2026 +ai-usage: ai-assisted no-loc: [System.Text.Json] ms.topic: reference --- @@ -89,6 +90,7 @@ The following sections are organized by namespace and show which types are suppo | | ✔️ | ✔️ | | \* | ✔️ | ✔️ | | | ✔️ | ✔️ | +| § | ✔️ | ✔️ | | | ✔️ | ✔️ | | | ✔️ | ✔️ | | | ✔️ | ✔️ | @@ -106,6 +108,8 @@ The following sections are organized by namespace and show which types are suppo ‡ See [Support round trip for `Stack` types](converters-how-to.md#support-round-trip-for-stack-types). +§ `System.Text.Json` supports in .NET 11 and later versions. When you deserialize the interface, the serializer creates a instance. For generated metadata, creates the collection contract. + #### IAsyncEnumerable\ The following examples use streams as a representation of any async source of data. The source could be files on a local machine, or results from a database query or web service API call. @@ -118,13 +122,22 @@ The following examples use streams as a representation of any async source of da `IAsyncEnumerable` values are only supported by the asynchronous serialization methods, such as . +In .NET 11 and later versions, writes an `IAsyncEnumerable` sequence to either a or a . With the default `topLevelValues: false`, the method writes a single root-level JSON array. Set `topLevelValues: true` to write [JSON Lines](https://jsonlines.org/) instead, where each element is a separate top-level value: + +```json +{"id":1,"name":"apple"} +{"id":2,"name":"banana"} +``` + +The method writes a single line feed (LF), `\n`, after every value, including the last. It always uses LF, regardless of . The method ignores , so each value remains on one line. + ##### Stream deserialization The `DeserializeAsyncEnumerable` method supports streaming deserialization, as shown in the following example: :::code language="csharp" source="snippets/supported-types/csharp/IAsyncEnumerableDeserialize.cs" highlight="11"::: -The `DeserializeAsyncEnumerable` method only supports reading from root-level JSON arrays. +By default, reads elements from a single root-level JSON array. Set `topLevelValues: true` to read a sequence of whitespace-separated top-level values instead. This input format is a superset of JSON Lines. Overloads accept either a or a . The method supports `IAsyncEnumerable`, but its signature doesn't allow streaming. It returns the final result as a single value, as shown in the following example. @@ -240,11 +253,15 @@ For more information about known issues, see the [open issues in System.Text.Jso When used as the keys of `Dictionary` and `SortedList` types, the following types have built-in support: +* (.NET 11 and later) * `Boolean` * `Byte` * `DateTime` * `DateTimeOffset` * `Decimal` +* (.NET 11 and later) +* (.NET 11 and later) +* (.NET 11 and later) * `Double` * `Enum` * `Guid` @@ -264,6 +281,35 @@ When used as the keys of `Dictionary` and `SortedList` types, the following type In addition, the and methods let you add dictionary key support for any type of your choosing. +## BFloat16 and decimal floating-point types + +Starting in .NET 11, `System.Text.Json` includes built-in converters for the , , , and types. Finite values serialize as JSON numbers. + +These types behave like the other built-in numeric types: + +* Dictionary-key conversion supports all four types. +* They honor , including the `"NaN"`, `"Infinity"`, and `"-Infinity"` literals through . + + exposes converter properties for source-generated metadata. The properties are , , , and . + +## F# discriminated unions + +Starting in .NET 11, `System.Text.Json` serializes and deserializes F# discriminated unions, including class, struct, and recursive unions: + +```fsharp +type Shape = + | Point + | Circle of radius: float +``` + +* A case without fields serializes as a JSON string that contains the case name, such as `"Point"`. +* A case that has fields serializes as a JSON object. The object contains a `$type` discriminator followed by the case's named fields, such as `{"$type":"Circle","radius":3.14}`. + + applies to case names and field names. A case-level takes precedence. To use a discriminator property name other than `$type`, set . + +> [!IMPORTANT] +> F# discriminated union support is reflection-only. It requires dynamic code and untrimmed reflection metadata. You can't use it with `System.Text.Json` source generation or Native AOT. + ## Unsupported types The following types aren't supported for serialization: diff --git a/docs/standard/serialization/system-text-json/union-types.md b/docs/standard/serialization/system-text-json/union-types.md new file mode 100644 index 0000000000000..a5ed4e9fc2aa5 --- /dev/null +++ b/docs/standard/serialization/system-text-json/union-types.md @@ -0,0 +1,123 @@ +--- +title: Serialize union types with System.Text.Json +description: Learn how System.Text.Json serializes and deserializes C# union types in .NET 11. +ms.date: 09/24/2026 +no-loc: [System.Text.Json] +dev_langs: + - "csharp" +ms.topic: how-to +ai-usage: ai-assisted +--- + +# Serialize union types with System.Text.Json + +Starting in .NET 11, supports [C# 15 union types](../../../csharp/language-reference/builtin-types/union.md). A union holds one of the case types in its declaration. `JsonSerializer` writes the active case value and can read it back. + +## Serialize and deserialize union values + +Declare a union whose cases have distinct JSON token types: + +```csharp +public union Payload(int, string, Message); +public sealed record Message(string Text); +``` + +Use `JsonSerializer` to serialize and deserialize the union: + +```csharp +Payload payload = new Message("Ready"); +string json = JsonSerializer.Serialize(payload); +Payload copy = JsonSerializer.Deserialize(json); +``` + +The serialized JSON contains the active case value rather than a wrapper or discriminator: + +```json +{"Text":"Ready"} +``` + +By default, the serializer classifies incoming JSON by token type. In the preceding union, a JSON number selects `int`, a JSON string selects `string`, and a JSON object selects `Message`. + +With , an `int` can also be read from a JSON string. Both the `int` and `string` cases of `Payload` then claim the string token type. Even `"25%"` throws for ambiguity before the serializer parses either case. To read strings with web defaults, [provide a custom classifier](#provide-a-custom-classifier) that chooses the case. + +## Distinguish cases with the same JSON token type + +Token classification can't distinguish two cases that both serialize as JSON objects. Apply and select to classify object cases by their property names: + +```csharp +[JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))] +public union Pet(Dog, Cat); +public sealed record Dog(string Name, string Breed); +public sealed record Cat(string Name, int Lives); +``` + +Here, `JsonUnionAttribute` selects a classifier for the existing `Pet` union; applying it to an ordinary type doesn't turn that type into a union. + +The classifier selects `Dog` when the payload contains `Breed` and selects `Cat` when it contains `Lives`: + +```csharp +Pet pet = JsonSerializer.Deserialize( + """{"Name":"Rex","Breed":"Husky"}"""); +``` + +For JSON objects, the structural classifier starts with the compatible object cases and narrows that set as it reads recognized root-level property names. Required properties remove cases when they're absent. removes a case when the payload contains a property that the case doesn't declare. Classification succeeds only when one case remains. + +The structural classifier doesn't inspect property values, nested objects, string contents, or array elements. Keep these consequences in mind: + +* A payload that leaves zero or multiple candidates throws . +* Overlapping or shadowed object contracts might be rejected when the classifier is created. +* Multiple non-object cases that use the same JSON token type aren't supported. For example, `Guid` and `string` both use JSON strings. +* A plain object case can't be mixed with a dictionary, `JsonObject`, or another non-POCO object-shaped case. +* Nested union cases and polymorphic cases aren't supported. +* isn't supported. +* A configuration that can't distinguish its cases throws when the serializer builds the classifier. + +## Choose unions or closed hierarchies + +Use a union when you need to preserve a discriminator-free JSON format you don't control, or when the cases have distinct JSON shapes. For example, the `int` and `string` cases of `Payload` are distinguishable by JSON token type. For object cases such as `Pet(Dog, Cat)`, however, changes to property names can affect which case a structural classifier selects. + +When you control the types and JSON contract, a [closed hierarchy with inferred polymorphism](polymorphism.md#infer-polymorphism-from-a-closed-hierarchy) can identify object cases with a discriminator instead: + +```csharp +[JsonPolymorphic(InferClosedTypePolymorphism = true)] +public closed record Event; +public sealed record Created(int Id) : Event; +public sealed record Deleted(int Id) : Event; +``` + +`JsonSerializer.Serialize(new Created(42))` writes `{"$type":"Created","Id":42}`. Both derived types declare `Id`, but the discriminator identifies the case independently of its properties. This makes case selection more stable as the properties evolve. Unlike union cases, the derived types must share a base class, and you must opt in to inferred polymorphism; the `closed` modifier alone doesn't add a discriminator. + +## Provide a custom classifier + +Derive from when default token classification or the built-in doesn't meet your requirements. A custom classifier can use other structural rules to select a union case. Register the factory in one of these locations: + +* Assign a delegate to when you customize the contract. +* Set for one union. +* Add the factory to for reflection-based serialization. +* Set for a source-generation context. + +A classifier reads the current JSON value and returns one of the case types from . The serializer checks the contract delegate first, followed by the per-union factory, the options-level factories, and built-in token classification. + +For ambiguous unions, the source generator reports a diagnostic unless a classifier is configured at generation time. + +## Handle null and default union values + +A union can declare nullable case types. JSON `null` selects the first nullable case. If the union has no nullable case, JSON `null` produces the default union value. For a compiler-generated struct union, the default value has no active case and serializes as JSON `null`. + +## Customize a union contract + +For advanced scenarios, customize the union metadata through . A union's value is . Its contract exposes: + +* , which contains entries. +* , which creates a union from a case type and value. +* , which returns the active case type and value. +* , which selects a case during deserialization. + +For more information about modifying `JsonTypeInfo`, see [Customize a JSON contract](custom-contracts.md). + +## See also + +* [Union types (C# reference)](../../../csharp/language-reference/builtin-types/union.md) +* [Serialize polymorphic types](polymorphism.md) +* [Use source generation](source-generation.md) +* [Use C# unions and closed hierarchies in ASP.NET Core (.NET Blog)](https://devblogs.microsoft.com/dotnet/unions-and-closed-hierarchies-in-aspnetcore/) diff --git a/docs/standard/serialization/system-text-json/use-utf8jsonwriter.md b/docs/standard/serialization/system-text-json/use-utf8jsonwriter.md index 654219a70d55c..2703596d57e2d 100644 --- a/docs/standard/serialization/system-text-json/use-utf8jsonwriter.md +++ b/docs/standard/serialization/system-text-json/use-utf8jsonwriter.md @@ -1,12 +1,13 @@ --- title: How to use Utf8JsonWriter in System.Text.Json description: "Learn how to use Utf8JsonWriter." -ms.date: 03/29/2022 +ms.date: 08/18/2026 no-loc: [System.Text.Json, Newtonsoft.Json] dev_langs: - "csharp" - "vb" ms.topic: how-to +ai-usage: ai-assisted --- # How to use Utf8JsonWriter in System.Text.Json @@ -20,6 +21,26 @@ The following example shows how to use the or to reuse a writer. These overloads change the destination and options without allocating another `Utf8JsonWriter`. + +Before you reset the writer, finish the current JSON payload and call . `Reset` clears the writer state and doesn't flush pending output: + +```csharp +writer.WriteEndObject(); +writer.Flush(); + +writer.Reset(nextStream, new JsonWriterOptions { Indented = true }); +``` + +```vb +writer.WriteEndObject() +writer.Flush() + +writer.Reset(nextStream, New JsonWriterOptions With {.Indented = True}) +``` + ## Write with UTF-8 text To achieve the best possible performance while using the `Utf8JsonWriter`, write JSON payloads already encoded as UTF-8 text rather than as UTF-16 strings. Use to cache and pre-encode known string property names and values as statics, and pass those to the writer, rather than using UTF-16 string literals. This is faster than caching and using UTF-8 byte arrays.