-
Notifications
You must be signed in to change notification settings - Fork 6k
Add contract customization article #31479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0d2acda
update for .net 7
gewarren e6f090b
add start of custom contracts article
gewarren 1217cec
finish up and add examples
gewarren 6c6942b
fix warnings
gewarren 8c325c4
Apply suggestions from code review
gewarren 1104f06
use xrefs instead
gewarren 01c19a0
Merge branch 'custom-contract' of github.com:gewarren/docs into custo…
gewarren 22ce7f9
use list item remover extension method
gewarren a44b5db
self review
gewarren 1a3d49f
typo
gewarren File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
120 changes: 120 additions & 0 deletions
120
docs/standard/serialization/system-text-json/custom-contracts.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <xref:System.Text.Json?displayProperty=fullName> 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 <xref:System.Collections.IEnumerable> or <xref:System.Collections.IDictionary> 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 vs. lack of a value in the JSON payload. | ||
gewarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<!--Add links to blog post when published.--> | ||
|
||
## How to opt in | ||
|
||
There are two ways to plug into customization. Both involve obtaining a resolver, whose job is to provide a <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo> instance for each type that needs to be serialized. | ||
|
||
- By calling the <xref:System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.%23ctor> constructor to obtain the <xref:System.Text.Json.JsonSerializerOptions.TypeInfoResolver?displayProperty=nameWithType> and adding your [custom actions](#modifiers) to its <xref:System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver.Modifiers> property. | ||
|
||
For example: | ||
|
||
```csharp | ||
JsonSerializerOptions options = new() | ||
{ | ||
TypeInfoResolver = new DefaultJsonTypeInfoResolver() | ||
gewarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
Modifiers = | ||
{ | ||
MyCustomModifier1, | ||
MyCustomModifier2 | ||
} | ||
} | ||
}; | ||
``` | ||
|
||
If you add multiple modifiers, they'll be called serially. | ||
gewarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- By writing a custom resolver that implements <xref:System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver>. | ||
|
||
- If a type isn't handled, <xref:System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver.GetTypeInfo%2A?displayProperty=nameWithType> 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 <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo> value is returned for the type. | ||
|
||
## Configurable aspects | ||
|
||
The <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo.Kind?displayProperty=nameWithType> 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 | | ||
| - | - | | ||
| [JsonTypeInfoKind.Object](/dotnet/api/system.text.json.serialization.metadata.jsontypeinfokind#system-text-json-serialization-metadata-jsontypeinfokind-object) | 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.** | | ||
gewarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [JsonTypeInfoKind.Enumerable](/dotnet/api/system.text.json.serialization.metadata.jsontypeinfokind#system-text-json-serialization-metadata-jsontypeinfokind-enumerable) | The converter will serialize the type into a JSON array. This kind is used for types like `List<T>` and array. | | ||
| [JsonTypeInfoKind.Dictionary](/dotnet/api/system.text.json.serialization.metadata.jsontypeinfokind#system-text-json-serialization-metadata-jsontypeinfokind-dictionary) | The converter will serialize the type into a JSON object. This kind is used for types like `Dictionary<K, V>`. | | ||
| [JsonTypeInfoKind.None](/dotnet/api/system.text.json.serialization.metadata.jsontypeinfokind#system-text-json-serialization-metadata-jsontypeinfokind-none) | 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 <xref:System.Object?displayProperty=nameWithType>, `int`, and `string`, and for all types that use a custom converter. | | ||
|
||
## Modifiers | ||
|
||
A modifier is an `Action<JsonTypeInfo>` or a method with a <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo> 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 <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo> to find the one you're interested in and then modify its <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Get?displayProperty=nameWithType> property (for serialization) or <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Set?displayProperty=nameWithType> property (for deserialization). Or, you can construct a new property using <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo.CreateJsonPropertyInfo(System.Type,System.String)?displayProperty=nameWithType> and add it to the <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo.Properties?displayProperty=nameWithType> 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 <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Get?displayProperty=nameWithType> delegate (for serialization) or <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Set?displayProperty=nameWithType> delegate (for deserialization) for the property. | [Increment a property's value](#example-increment-a-propertys-value) | | ||
| Add or remove properties | `JsonTypeInfoKind.Object` | [Serialize private fields](#example-serialize-private-fields) | | ||
gewarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Conditionally serialize a property | `JsonTypeInfoKind.Object` | Modify the <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.ShouldSerialize?displayProperty=nameWithType> 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 <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo.NumberHandling?displayProperty=nameWithType> 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 <xref:System.Text.Json.Serialization.Metadata.JsonPropertyInfo.Set?displayProperty=nameWithType> 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 <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo>. | ||
|
||
:::code language="csharp" source="snippets/custom-contracts/PrivateFields.cs"::: | ||
|
||
> [!TIP] | ||
> If your private field names start with underscores, consider adding a JSON naming policy to remove the underscore and capitalize the name. | ||
gewarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## 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 first clearing the <xref:System.Text.Json.Serialization.Metadata.JsonTypeInfo.Properties?displayProperty=nameWithType> list and then readding only those properties that don't have the specified type. The filtered properties will completely disappear from the contract, which means `System.Text.Json` won'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 <xref:System.Text.Json.Serialization.JsonNumberHandlingAttribute> 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 is hard. Using contract customization, you can customize the number handling behavior for any type. | ||
gewarren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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: | ||
|
||
> 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 <xref:System.Text.Json.Serialization.JsonAttribute>, for example, <xref:System.Text.Json.Serialization.JsonIgnoreAttribute> and <xref:System.Text.Json.Serialization.JsonPropertyOrderAttribute>. | ||
- By modifying <xref:System.Text.Json.JsonSerializerOptions>, 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) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.