diff --git a/docs/fundamentals/toc.yml b/docs/fundamentals/toc.yml index 4db00f735ff5d..817bf77ae730d 100644 --- a/docs/fundamentals/toc.yml +++ b/docs/fundamentals/toc.yml @@ -2344,6 +2344,8 @@ items: href: ../standard/serialization/system-text-json/source-generation.md - name: Write custom converters href: ../standard/serialization/system-text-json/converters-how-to.md + - name: Customize contracts + href: ../standard/serialization/system-text-json/custom-contracts.md - name: Binary serialization items: - name: Overview 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 74c8faf813021..3ed40e9aabd98 100644 --- a/docs/standard/serialization/system-text-json/converters-how-to.md +++ b/docs/standard/serialization/system-text-json/converters-how-to.md @@ -236,7 +236,6 @@ The following sections provide converter samples that address some common scenar ::: zone pivot="dotnet-7-0" * [Deserialize inferred types to object properties](#deserialize-inferred-types-to-object-properties). -* [Support polymorphic deserialization](#support-polymorphic-deserialization). * [Support round-trip for Stack\](#support-round-trip-for-stackt). * [Support enum string value deserialization](#support-enum-string-value-deserialization). * [Use default system converter](#use-default-system-converter). @@ -353,7 +352,7 @@ The [unit tests folder](https://github.com/dotnet/runtime/blob/81bf79fd9aa75305e ### Support polymorphic deserialization -Built-in features provide a limited range of [polymorphic serialization](polymorphism.md) but no support for deserialization at all. Deserialization requires a custom converter. +.NET 7 provides support for both [polymorphic serialization and deserialization](polymorphism.md). However, in previous .NET versions, there was limited polymorphic serialization support and no support for deserialization. If you're using .NET 6 or an earlier version, deserialization requires a custom converter. Suppose, for example, you have a `Person` abstract base class, with `Employee` and `Customer` derived classes. Polymorphic deserialization means that at design time you can specify `Person` as the deserialization target, and `Customer` and `Employee` objects in the JSON are correctly deserialized at run time. During deserialization, you have to find clues that identify the required type in the JSON. The kinds of clues available vary with each scenario. For example, a discriminator property might be available or you might have to rely on the presence or absence of a particular property. The current release of `System.Text.Json` doesn't provide attributes to specify how to handle polymorphic deserialization scenarios, so custom converters are required. diff --git a/docs/standard/serialization/system-text-json/custom-contracts.md b/docs/standard/serialization/system-text-json/custom-contracts.md new file mode 100644 index 0000000000000..74a110f95fe3a --- /dev/null +++ b/docs/standard/serialization/system-text-json/custom-contracts.md @@ -0,0 +1,120 @@ +--- +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: 09/26/2022 +--- +# Customize a JSON contract + +The library constructs a JSON *contract* for each .NET type, which defines how the type should be serialized and deserialized. The contract is derived from the type's shape, which includes characteristics such as its properties and fields and whether it implements the or interface. Types are mapped to contracts either at run time using reflection or at compile time using the source generator. + +Starting in .NET 7, you can customize these JSON contracts to provide more control over how types are converted into JSON and vice versa. The following list shows just some examples of the types of customizations you can make to serialization and deserialization: + +- Serialize private fields and properties. +- Support multiple names for a single property (for example, if a previous library version used a different name). +- Ignore properties with a specific name, type, or value. +- Distinguish between explicit `null` values and the lack of a value in the JSON payload. + + +## How to opt in + +There are two ways to plug into customization. Both involve obtaining a resolver, whose job is to provide a instance for each type that needs to be serialized. + +- By calling the constructor to obtain the and adding your [custom actions](#modifiers) to its property. + + For example: + + ```csharp + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + MyCustomModifier1, + MyCustomModifier2 + } + } + }; + ``` + + If you add multiple modifiers, they'll be called sequentially. + +- By writing a custom resolver that implements . + + - 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. + +## 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: + +| `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 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 + +A modifier is an `Action` or a method with a parameter that gets the current state of the contract as an argument and makes modifications to the contract. For example, you could iterate through the prepopulated properties on the specified to find the one you're interested in and then modify its property (for serialization) or property (for deserialization). Or, you can construct a new property using and add it to the collection. + +The following table shows the modifications you can make and how to achieve them. + +| Modification | Applicable `JsonTypeInfo.Kind` | How to achieve it | Example | +| - | - | - | - | +| Customize a property's value | `JsonTypeInfoKind.Object` | Modify the delegate (for serialization) or delegate (for deserialization) for the property. | [Increment a property's value](#example-increment-a-propertys-value) | +| 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) | + +## Example: Increment a property's value + +Consider the following example where the modifier increments the value of a certain property on deserialization by modifying its delegate. Besides defining the modifier, the example also introduces a new attribute that it uses to locate the property whose value should be incremented. This is an example of *customizing a property*. + +:::code language="csharp" source="snippets/custom-contracts/SerializationCount.cs"::: + +Notice in the output that the value of `RoundTrips` is incremented each time the `Product` instance is deserialized. + +## Example: Serialize private fields + +By default, `System.Text.Json` ignores private fields and properties. This example adds a new class-wide attribute, `JsonIncludePrivateFieldsAttribute`, to change that default. If the modifier finds the attribute on a type, it adds all the private fields on the type as new properties to . + +:::code language="csharp" source="snippets/custom-contracts/PrivateFields.cs"::: + +> [!TIP] +> If your private field names start with underscores, consider removing the underscores from the names when you add the fields as new JSON properties. + +## Example: Ignore properties with a specific type + +Perhaps your model has properties with specific names or types that you don't want to expose to users. For example, you might have a property that stores credentials or some information that's useless to have in the payload. + +The following example shows how to filter out properties with a specific type, `SecretHolder`. It does this by using an extension method to remove any properties that have the specified type from the list. The filtered properties completely disappear from the contract, which means `System.Text.Json` doesn't look at them either during serialization or deserialization. + +:::code language="csharp" source="snippets/custom-contracts/IgnoreType.cs"::: + +## Example: Allow int values to be strings + +Perhaps your input JSON can contain quotes around one of the numeric types but not on others. If you had control over the class, you could place on the type to fix this, but you don't. Before .NET 7, you'd need to write a [custom converter](converters-how-to.md) to fix this behavior, which requires writing a fair bit of code. Using contract customization, you can customize the number handling behavior for any type. + +The following example changes the behavior for all `int` values. The example can be easily adjusted to apply to any type or for a specific property of any type. + +:::code language="csharp" source="snippets/custom-contracts/ReadIntFromString.cs"::: + +Without the modifier to allow reading `int` values from a string, the program would have ended with an exception: + +> Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to System.Int32. Path: $.X | LineNumber: 0 | BytePositionInLine: 9. + +## Other ways to customize serialization + +Besides customizing a contract, there are other ways to influence serialization and deserialization behavior, including the following: + +- By using attributes derived from , for example, and . +- By modifying , for example, to set a naming policy or serialize enumeration values as strings instead of numbers. +- By writing a custom converter that does the actual work of writing the JSON and, during deserialization, constructing an object. + +Contract customization is an improvement over these pre-existing customizations because you might not have access to the type to add attributes, and writing a custom converter is complex and hurts performance. + +## See also + +- [JSON contract customization blog post](https://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-6/#json-contract-customization) diff --git a/docs/standard/serialization/system-text-json/migrate-from-newtonsoft.md b/docs/standard/serialization/system-text-json/migrate-from-newtonsoft.md index 6966590433106..94aa564c78c81 100644 --- a/docs/standard/serialization/system-text-json/migrate-from-newtonsoft.md +++ b/docs/standard/serialization/system-text-json/migrate-from-newtonsoft.md @@ -2,7 +2,7 @@ title: "Migrate from Newtonsoft.Json to System.Text.Json - .NET" description: "Learn how to migrate from Newtonsoft.Json to System.Text.Json. Includes sample code." no-loc: [System.Text.Json, Newtonsoft.Json] -ms.date: 11/30/2021 +ms.date: 09/26/2022 zone_pivot_groups: dotnet-version helpviewer_keywords: - "JSON serialization" @@ -38,7 +38,7 @@ The following table lists `Newtonsoft.Json` features and `System.Text.Json` equi * Not supported, workaround is possible. The workarounds are [custom converters](converters-how-to.md), which may not provide complete parity with `Newtonsoft.Json` functionality. For some of these, sample code is provided as examples. If you rely on these `Newtonsoft.Json` features, migration will require modifications to your .NET object models or other code changes. * Not supported, workaround is not practical or possible. If you rely on these `Newtonsoft.Json` features, migration will not be possible without significant changes. -::: zone pivot="dotnet-7-0,dotnet-6-0" +::: zone pivot="dotnet-7-0" | Newtonsoft.Json feature | System.Text.Json equivalent | |-------------------------------------------------------|-----------------------------| @@ -63,13 +63,13 @@ The following table lists `Newtonsoft.Json` features and `System.Text.Json` equi | `ReferenceLoopHandling` global setting | ✔️ [ReferenceHandling global setting](#preserve-object-references-and-handle-loops) | | Callbacks | ✔️ [Callbacks](#callbacks) | | NaN, Infinity, -Infinity | ✔️ [Supported](#nan-infinity--infinity) | +| `Required` setting on `[JsonProperty]` attribute | ✔️ [[JsonRequired] attribute and C# required modifier](#required-properties) | +| `DefaultContractResolver` to ignore properties | ✔️ [DefaultJsonTypeInfoResolver class](#conditionally-ignore-a-property) | +| Polymorphic serialization | ✔️ [[JsonDerivedType] attribute](#polymorphic-serialization) | +| Polymorphic deserialization | ✔️ [Type discriminator on [JsonDerivedType] attribute](#polymorphic-deserialization) | | Support for a broad range of types | ⚠️ [Some types require custom converters](#types-without-built-in-support) | -| Polymorphic serialization | ⚠️ [Not supported, workaround, sample](#polymorphic-serialization) | -| Polymorphic deserialization | ⚠️ [Not supported, workaround, sample](#polymorphic-deserialization) | | Deserialize inferred type to `object` properties | ⚠️ [Not supported, workaround, sample](#deserialization-of-object-properties) | | Deserialize JSON `null` literal to non-nullable value types | ⚠️ [Not supported, workaround, sample](#deserialize-null-to-non-nullable-type) | -| `Required` setting on `[JsonProperty]` attribute | ⚠️ [Not supported, workaround, sample](#required-properties) | -| `DefaultContractResolver` to ignore properties | ⚠️ [Not supported, workaround, sample](#conditionally-ignore-a-property) | | `DateTimeZoneHandling`, `DateFormatString` settings | ⚠️ [Not supported, workaround, sample](#specify-date-format) | | `JsonConvert.PopulateObject` method | ⚠️ [Not supported, workaround](#populate-existing-objects) | | `ObjectCreationHandling` global setting | ⚠️ [Not supported, workaround](#reuse-rather-than-replace-properties) | @@ -85,7 +85,7 @@ The following table lists `Newtonsoft.Json` features and `System.Text.Json` equi | Configurable limits | ❌ [Not supported](#some-limits-not-configurable) | ::: zone-end -::: zone pivot="dotnet-5-0" +::: zone pivot="dotnet-6-0" | Newtonsoft.Json feature | System.Text.Json equivalent | |-------------------------------------------------------|-----------------------------| @@ -107,6 +107,8 @@ The following table lists `Newtonsoft.Json` features and `System.Text.Json` equi | Deserialize `Dictionary` with non-string key | ✔️ [Supported](#dictionary-with-non-string-key) | | Support for non-public property setters and getters | ✔️ [JsonInclude attribute](#non-public-property-setters-and-getters) | | `[JsonConstructor]` attribute | ✔️ [[JsonConstructor] attribute](#specify-constructor-to-use-when-deserializing) | +| `ReferenceLoopHandling` global setting | ✔️ [ReferenceHandling global setting](#preserve-object-references-and-handle-loops) | +| Callbacks | ✔️ [Callbacks](#callbacks) | | NaN, Infinity, -Infinity | ✔️ [Supported](#nan-infinity--infinity) | | Support for a broad range of types | ⚠️ [Some types require custom converters](#types-without-built-in-support) | | Polymorphic serialization | ⚠️ [Not supported, workaround, sample](#polymorphic-serialization) | @@ -116,12 +118,10 @@ The following table lists `Newtonsoft.Json` features and `System.Text.Json` equi | `Required` setting on `[JsonProperty]` attribute | ⚠️ [Not supported, workaround, sample](#required-properties) | | `DefaultContractResolver` to ignore properties | ⚠️ [Not supported, workaround, sample](#conditionally-ignore-a-property) | | `DateTimeZoneHandling`, `DateFormatString` settings | ⚠️ [Not supported, workaround, sample](#specify-date-format) | -| Callbacks | ⚠️ [Not supported, workaround, sample](#callbacks) | | `JsonConvert.PopulateObject` method | ⚠️ [Not supported, workaround](#populate-existing-objects) | | `ObjectCreationHandling` global setting | ⚠️ [Not supported, workaround](#reuse-rather-than-replace-properties) | | Add to collections without setters | ⚠️ [Not supported, workaround](#add-to-collections-without-setters) | | Snake-case property names | ⚠️ [Not supported, workaround](#snake-case-naming-policy)| -| `ReferenceLoopHandling` global setting | ❌ [Not supported](#preserve-object-references-and-handle-loops) | | Support for `System.Runtime.Serialization` attributes | ❌ [Not supported](#systemruntimeserialization-attributes) | | `MissingMemberHandling` global setting | ❌ [Not supported](#missingmemberhandling) | | Allow property names without quotes | ❌ [Not supported](#json-strings-property-names-and-string-values) | @@ -336,6 +336,8 @@ The `Newtonsoft.Json` `[JsonConstructor]` attribute lets you specify which const * If you're [Including fields](how-to.md#include-fields), the global option lets you ignore all read-only fields. * The `DefaultIgnoreCondition` global option lets you [ignore all value type properties that have default values](ignore-properties.md#ignore-all-default-value-properties), or [ignore all reference type properties that have null values](ignore-properties.md#ignore-all-null-value-properties). +In addition, in .NET 7 and later versions, you can customize the JSON contract to ignore properties based on arbitrary criteria. For more information, see [Custom contracts](custom-contracts.md). + ::: zone-end ::: zone pivot="dotnet-core-3-1" @@ -347,23 +349,17 @@ The `Newtonsoft.Json` `[JsonConstructor]` attribute lets you specify which const * The [IgnoreReadOnlyProperties](ignore-properties.md#ignore-all-read-only-properties) global option lets you ignore all read-only properties. ::: zone-end -These options **don't** let you: +::: zone pivot="dotnet-core-3-1,dotnet-5-0,dotnet-6-0" -::: zone pivot="dotnet-5-0,dotnet-7-0,dotnet-6-0" +These options **don't** let you ignore selected properties based on arbitrary criteria evaluated at run time. -* Ignore selected properties based on arbitrary criteria evaluated at run time. - -::: zone-end - -::: zone pivot="dotnet-core-3-1" +In addition, in .NET Core 3.1 you can't: * Ignore all properties that have the default value for the type. * Ignore selected properties that have the default value for the type. * Ignore selected properties if their value is null. * Ignore selected properties based on arbitrary criteria evaluated at run time. -::: zone-end - For that functionality, you can write a custom converter. Here's a sample POCO and a custom converter for it that illustrates this approach: :::code language="csharp" source="snippets/system-text-json-how-to/csharp/WeatherForecast.cs" id="WF"::: @@ -379,6 +375,8 @@ This approach requires additional logic if: * The POCO includes complex properties. * You need to handle attributes such as `[JsonIgnore]` or options such as custom encoders. +::: zone-end + ### Public and non-public fields `Newtonsoft.Json` can serialize and deserialize fields as well as properties. @@ -478,15 +476,13 @@ Custom converters can be implemented for types that don't have built-in support. ### Polymorphic serialization -`Newtonsoft.Json` automatically does polymorphic serialization. For information about the limited polymorphic serialization capabilities of , see [Serialize properties of derived classes](polymorphism.md). - -The workaround described there is to define properties that may contain derived classes as type `object`. If that isn't possible, another option is to create a converter with a `Write` method for the whole inheritance type hierarchy like the example in [How to write custom converters](converters-how-to.md#support-polymorphic-deserialization). +`Newtonsoft.Json` automatically does polymorphic serialization. Starting in .NET 7, supports polymorphic serialization through the attribute. For more information, see [Serialize properties of derived classes](polymorphism.md). ### Polymorphic deserialization -`Newtonsoft.Json` has a `TypeNameHandling` setting that adds type name metadata to the JSON while serializing. It uses the metadata while deserializing to do polymorphic deserialization. can do a limited range of [polymorphic serialization](polymorphism.md) but not polymorphic deserialization. +`Newtonsoft.Json` has a `TypeNameHandling` setting that adds type-name metadata to the JSON while serializing. It uses the metadata while deserializing to do polymorphic deserialization. Starting in .NET 7, relies on type discriminator information to perform polymorphic deserialization. This metadata is emitted in the JSON and then used during deserialization to determine whether to deserialize to the base type or a derived type. For more information, see [Serialize properties of derived classes](polymorphism.md). -To support polymorphic deserialization, create a converter like the example in [How to write custom converters](converters-how-to.md#support-polymorphic-deserialization). +To support polymorphic deserialization in older .NET versions, create a converter like the example in [How to write custom converters](converters-how-to.md#support-polymorphic-deserialization). ### Deserialization of object properties @@ -568,6 +564,14 @@ For an example of a similar converter that handles open generic properties, see In `Newtonsoft.Json`, you specify that a property is required by setting `Required` on the `[JsonProperty]` attribute. `Newtonsoft.Json` throws an exception if no value is received in the JSON for a property marked as required. +::: zone pivot="dotnet-7-0" + +Starting in .NET 7, you can use the C# `required` modifier or the attribute on a required property. System.Text.Json throws an exception if the JSON payload doesn't contain a value for the marked property. For more information, see [Required properties](required-properties.md). + +::: zone-end + +::: zone pivot="dotnet-core-3-1,dotnet-5-0,dotnet-6-0" + doesn't throw an exception if no value is received for one of the properties of the target type. For example, if you have a `WeatherForecast` class: :::code language="csharp" source="snippets/system-text-json-how-to/csharp/WeatherForecast.cs" id="WF"::: @@ -583,6 +587,7 @@ The following JSON is deserialized without error: To make deserialization fail if no `Date` property is in the JSON, choose one of the following options: +* Use the .NET 7 or later version of the [System.Text.Json package](https://www.nuget.org/packages/System.Text.Json) and add the `required` modifier (available starting in C# 11) or the attribute to the property. * Implement a custom converter. * Implement an [`OnDeserialized` callback (.NET 6 and later)](migrate-from-newtonsoft.md?pivots=dotnet-6-0#callbacks). @@ -616,7 +621,10 @@ The required properties converter would require additional logic if you need to * A property for a non-nullable type is present in the JSON, but the value is the default for the type, such as zero for an `int`. * A property for a nullable value type is present in the JSON, but the value is null. -**Note:** If you're using System.Text.Json from an ASP.NET Core controller, you might be able to use a [`[Required]`](xref:System.ComponentModel.DataAnnotations.RequiredAttribute) attribute on properties of the model class instead of implementing a System.Text.Json converter. +> [!NOTE] +> If you're using System.Text.Json from an ASP.NET Core controller, you might be able to use a [`[Required]`](xref:System.ComponentModel.DataAnnotations.RequiredAttribute) attribute on properties of the model class instead of implementing a System.Text.Json converter. + +::: zone-end ### Specify date format @@ -680,7 +688,7 @@ For more information about custom converters that recursively call `Serialize` o `Newtonsoft.Json` can use private and internal property setters and getters via the `JsonProperty` attribute. ::: zone pivot="dotnet-5-0,dotnet-7-0,dotnet-6-0" - supports private and internal property setters and getters via the [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute) attribute. For sample code, see [Non-public property accessors](immutability.md). + supports private and internal property setters and getters via the [[JsonInclude]](xref:System.Text.Json.Serialization.JsonIncludeAttribute) attribute. For sample code, see [Non-public property accessors](immutability.md#non-public-property-accessors). ::: zone-end ::: zone pivot="dotnet-core-3-1" @@ -693,7 +701,7 @@ The `JsonConvert.PopulateObject` method in `Newtonsoft.Json` deserializes a JSON ### Reuse rather than replace properties -The `Newtonsoft.Json` `ObjectCreationHandling` setting lets you specify that objects in properties should be reused rather than replaced during deserialization. always replaces objects in properties. Custom converters can provide this functionality. +The `Newtonsoft.Json` `ObjectCreationHandling` setting lets you specify that objects in properties should be reused rather than replaced during deserialization. always replaces objects in properties. Custom converters can provide this functionality. ### Add to collections without setters @@ -786,7 +794,7 @@ The `Newtonsoft.Json` `WriteRawValue` method writes raw JSON where a value is ex ::: zone pivot="dotnet-5-0,dotnet-core-3-1" -The `Newtonsoft.Json` `WriteRawValue` method writes raw JSON where a value is expected. There is an equivalent method, , in .NET 6. For more information, see [Write raw JSON](use-dom-utf8jsonreader-utf8jsonwriter.md?pivots=dotnet-6-0#write-raw-json). +The `Newtonsoft.Json` `WriteRawValue` method writes raw JSON where a value is expected. There is an equivalent method, , in .NET 6 and later versions. For more information, see [Write raw JSON](use-dom-utf8jsonreader-utf8jsonwriter.md?pivots=dotnet-6-0#write-raw-json). For versions earlier than 6.0, has no equivalent method for writing raw JSON. However, the following workaround ensures only valid JSON is written: @@ -801,10 +809,10 @@ doc.WriteTo(writer); `JsonTextWriter` includes the following settings, for which `Utf8JsonWriter` has no equivalent: -* [Indentation](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_Indentation.htm) - Specifies how many characters to indent. `Utf8JsonWriter` always does 2-character indentation. -* [IndentChar](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_IndentChar.htm) - Specifies the character to use for indentation. `Utf8JsonWriter` always uses whitespace. -* [QuoteChar](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_QuoteChar.htm) - Specifies the character to use to surround string values. `Utf8JsonWriter` always uses double quotes. -* [QuoteName](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_QuoteName.htm) - Specifies whether or not to surround property names with quotes. `Utf8JsonWriter` always surrounds them with quotes. +* [Indentation](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_Indentation.htm) - Specifies how many characters to indent. `Utf8JsonWriter` always indents by 2 characters. +* [IndentChar](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_IndentChar.htm) - Specifies the character to use for indentation. `Utf8JsonWriter` always uses whitespace. +* [QuoteChar](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_QuoteChar.htm) - Specifies the character to use to surround string values. `Utf8JsonWriter` always uses double quotes. +* [QuoteName](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonTextWriter_QuoteName.htm) - Specifies whether or not to surround property names with quotes. `Utf8JsonWriter` always surrounds them with quotes. There are no workarounds that would let you customize the JSON produced by `Utf8JsonWriter` in these ways. diff --git a/docs/standard/serialization/system-text-json/preserve-references.md b/docs/standard/serialization/system-text-json/preserve-references.md index 65932fd5f37fa..3416e9e9e9b8b 100644 --- a/docs/standard/serialization/system-text-json/preserve-references.md +++ b/docs/standard/serialization/system-text-json/preserve-references.md @@ -52,8 +52,8 @@ The class defines the be By default, reference data is only cached for each call to or . To persist references from one `Serialize`/`Deserialize` call to another one, root the instance in the call site of `Serialize`/`Deserialize`. The following code shows an example for this scenario: -* You have a list of `Employee`s and you have to serialize each one individually. -* you want to take advantage of the references saved in the `ReferenceHandler`'s resolver. +* You have a list of `Employee` objects and you have to serialize each one individually. +* You want to take advantage of the references saved in the resolver for the `ReferenceHandler`. Here is the `Employee` class: diff --git a/docs/standard/serialization/system-text-json/snippets/custom-contracts/IgnoreType.cs b/docs/standard/serialization/system-text-json/snippets/custom-contracts/IgnoreType.cs new file mode 100644 index 0000000000000..5b76c70e7c563 --- /dev/null +++ b/docs/standard/serialization/system-text-json/snippets/custom-contracts/IgnoreType.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Serialization +{ + class ExampleClass + { + public string Name { get; set; } = ""; + public SecretHolder? Secret { get; set; } + } + + class SecretHolder + { + public string Value { get; set; } = ""; + } + + class IgnorePropertiesWithType + { + private readonly Type[] _ignoredTypes; + + public IgnorePropertiesWithType(params Type[] ignoredTypes) + => _ignoredTypes = ignoredTypes; + + public void ModifyTypeInfo(JsonTypeInfo ti) + { + if (ti.Kind != JsonTypeInfoKind.Object) + return; + + ti.Properties.RemoveAll(prop => _ignoredTypes.Contains(prop.PropertyType)); + } + } + + public class IgnoreTypeExample + { + public static void RunIt() + { + var modifier = new IgnorePropertiesWithType(typeof(SecretHolder)); + + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { modifier.ModifyTypeInfo } + } + }; + + ExampleClass obj = new() + { + Name = "Password", + Secret = new SecretHolder { Value = "MySecret" } + }; + + string output = JsonSerializer.Serialize(obj, options); + Console.WriteLine(output); + // {"Name":"Password"} + } + } + + public static class ListHelpers + { + // IList implementation of List.RemoveAll method. + public static void RemoveAll(this IList list, Predicate predicate) + { + for (int i = 0; i < list.Count; i++) + { + if (predicate(list[i])) + { + list.RemoveAt(i--); + } + } + } + } +} diff --git a/docs/standard/serialization/system-text-json/snippets/custom-contracts/PrivateFields.cs b/docs/standard/serialization/system-text-json/snippets/custom-contracts/PrivateFields.cs new file mode 100644 index 0000000000000..4fcb588958306 --- /dev/null +++ b/docs/standard/serialization/system-text-json/snippets/custom-contracts/PrivateFields.cs @@ -0,0 +1,90 @@ +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Serialization +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] + public class JsonIncludePrivateFieldsAttribute : Attribute { } + + [JsonIncludePrivateFields] + public class Human + { + private string _name; + private int _age; + + public Human() + { + // This constructor should be used only by deserializers. + _name = null!; + _age = 0; + } + + public static Human Create(string name, int age) + { + Human h = new() + { + _name = name, + _age = age + }; + + return h; + } + + [JsonIgnore] + public string Name + { + get => _name; + set => throw new NotSupportedException(); + } + + [JsonIgnore] + public int Age + { + get => _age; + set => throw new NotSupportedException(); + } + } + + public class PrivateFieldsExample + { + static void AddPrivateFieldsModifier(JsonTypeInfo jsonTypeInfo) + { + if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object) + return; + + if (!jsonTypeInfo.Type.IsDefined(typeof(JsonIncludePrivateFieldsAttribute), inherit: false)) + return; + + foreach (FieldInfo field in jsonTypeInfo.Type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)) + { + JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(field.FieldType, field.Name); + jsonPropertyInfo.Get = field.GetValue; + jsonPropertyInfo.Set = field.SetValue; + + jsonTypeInfo.Properties.Add(jsonPropertyInfo); + } + } + + public static void RunIt() + { + var options = new JsonSerializerOptions + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { AddPrivateFieldsModifier } + } + }; + + var human = Human.Create("Julius", 37); + string json = JsonSerializer.Serialize(human, options); + Console.WriteLine(json); + // {"_name":"Julius","_age":37} + + Human deserializedHuman = JsonSerializer.Deserialize(json, options)!; + Console.WriteLine($"[Name={deserializedHuman.Name}; Age={deserializedHuman.Age}]"); + // [Name=Julius; Age=37] + } + } +} diff --git a/docs/standard/serialization/system-text-json/snippets/custom-contracts/Program.cs b/docs/standard/serialization/system-text-json/snippets/custom-contracts/Program.cs new file mode 100644 index 0000000000000..61d31595c0a81 --- /dev/null +++ b/docs/standard/serialization/system-text-json/snippets/custom-contracts/Program.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serialization +{ + public class Program + { + static void Main(string[] args) + { + SerializationCountExample.RunIt(); + Console.WriteLine(); + PrivateFieldsExample.RunIt(); + Console.WriteLine(); + IgnoreTypeExample.RunIt(); + Console.WriteLine(); + AllowIntsAsStringsExample.RunIt(); + } + } +} diff --git a/docs/standard/serialization/system-text-json/snippets/custom-contracts/Project.csproj b/docs/standard/serialization/system-text-json/snippets/custom-contracts/Project.csproj new file mode 100644 index 0000000000000..8911129a946d4 --- /dev/null +++ b/docs/standard/serialization/system-text-json/snippets/custom-contracts/Project.csproj @@ -0,0 +1,9 @@ + + + + Exe + net7.0 + enable + enable + + diff --git a/docs/standard/serialization/system-text-json/snippets/custom-contracts/ReadIntFromString.cs b/docs/standard/serialization/system-text-json/snippets/custom-contracts/ReadIntFromString.cs new file mode 100644 index 0000000000000..c7f16e49d4e41 --- /dev/null +++ b/docs/standard/serialization/system-text-json/snippets/custom-contracts/ReadIntFromString.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Serialization +{ + public class Point + { + public int X { get; set; } + public int Y { get; set; } + } + + public class AllowIntsAsStringsExample + { + static void SetNumberHandlingModifier(JsonTypeInfo jsonTypeInfo) + { + if (jsonTypeInfo.Type == typeof(int)) + { + jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString; + } + } + + public static void RunIt() + { + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { SetNumberHandlingModifier } + } + }; + + // Triple-quote syntax is a C# 11 feature. + Point point = JsonSerializer.Deserialize("""{"X":"12","Y":"3"}""", options)!; + Console.WriteLine($"({point.X},{point.Y})"); + // (12,3) + } + } +} diff --git a/docs/standard/serialization/system-text-json/snippets/custom-contracts/SerializationCount.cs b/docs/standard/serialization/system-text-json/snippets/custom-contracts/SerializationCount.cs new file mode 100644 index 0000000000000..2c90ae91c28ae --- /dev/null +++ b/docs/standard/serialization/system-text-json/snippets/custom-contracts/SerializationCount.cs @@ -0,0 +1,89 @@ +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Serialization +{ + // Custom attribute to annotate the property + // we want to be incremented. + [AttributeUsage(AttributeTargets.Property)] + class SerializationCountAttribute : Attribute + { + } + + // Example type to serialize and deserialize. + class Product + { + public string Name { get; set; } = ""; + [SerializationCount] + public int RoundTrips { get; set; } + } + + public class SerializationCountExample + { + // Custom modifier that increments the value + // of a specific property on deserialization. + static void IncrementCounterModifier(JsonTypeInfo typeInfo) + { + foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties) + { + if (propertyInfo.PropertyType != typeof(int)) + continue; + + object[] serializationCountAttributes = propertyInfo.AttributeProvider?.GetCustomAttributes(typeof(SerializationCountAttribute), true) ?? Array.Empty(); + SerializationCountAttribute? attribute = serializationCountAttributes.Length == 1 ? (SerializationCountAttribute)serializationCountAttributes[0] : null; + + if (attribute != null) + { + Action? setProperty = propertyInfo.Set; + if (setProperty is not null) + { + propertyInfo.Set = (obj, value) => + { + if (value != null) + { + // Increment the value by 1. + value = (int)value + 1; + } + + setProperty (obj, value); + }; + } + } + } + } + + public static void RunIt() + { + var product = new Product + { + Name = "Aquafresh" + }; + + JsonSerializerOptions options = new() + { + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { IncrementCounterModifier } + } + }; + + // First serialization and deserialization. + string serialized = JsonSerializer.Serialize(product, options); + Console.WriteLine(serialized); + // {"Name":"Aquafresh","RoundTrips":0} + + Product deserialized = JsonSerializer.Deserialize(serialized, options)!; + Console.WriteLine($"{deserialized.RoundTrips}"); + // 1 + + // Second serialization and deserialization. + serialized = JsonSerializer.Serialize(deserialized, options); + Console.WriteLine(serialized); + // { "Name":"Aquafresh","RoundTrips":1} + + deserialized = JsonSerializer.Deserialize(serialized, options)!; + Console.WriteLine($"{deserialized.RoundTrips}"); + // 2 + } + } +}