Skip to content
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

Sourcegen serialization #31132

Merged
merged 4 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/core/compatibility/7.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ If you're migrating an app to .NET 7, the breaking changes listed here might aff
| - | :-: | :-: | - |
| [DataContractSerializer retains sign when deserializing -0](serialization/7.0/datacontractserializer-negative-sign.md) | ❌ | ✔️ | RC 1 |
| [Deserialize Version type with leading or trailing whitespace](serialization/7.0/deserialize-version-with-whitespace.md) | ❌ | ✔️ | Preview 1 |
| [JsonSerializerOptions copy constructor includes JsonSerializerContext](serialization/7.0/jsonserializeroptions-copy-constructor.md) | ❌ | ✔️ | Preview 7 |
| [Polymorphic serialization for object types](serialization/7.0/polymorphic-serialization.md) | ❌ | ✔️ | RC 1 |
| [System.Text.Json source generator fallback](serialization/7.0/reflection-fallback.md) | ❌ | ✔️ | Preview 7 |

## Windows Forms

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
title: "Breaking change: JsonSerializerOptions copy constructor includes JsonSerializerContext"
description: Learn about the .NET 7 breaking change in serialization where the JsonSerializerOptions copy constructor now includes JsonSerializerContext.
ms.date: 09/12/2022
---
# JsonSerializerOptions copy constructor includes JsonSerializerContext

With the release of source generation in .NET 6, the <xref:System.Text.Json.JsonSerializerOptions> copy constructor was intentionally made to ignore its <xref:System.Text.Json.Serialization.JsonSerializerContext> state. This made sense at the time since <xref:System.Text.Json.Serialization.JsonSerializerContext> was designed to have a 1:1 relationship with <xref:System.Text.Json.JsonSerializerOptions> instances. In .NET 7, <xref:System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver> replaces <xref:System.Text.Json.Serialization.JsonSerializerContext> to generalize the context, which removes the need for tight coupling between <xref:System.Text.Json.JsonSerializerOptions> and <xref:System.Text.Json.Serialization.JsonSerializerContext>. The copy constructor now includes the <xref:System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver>/<xref:System.Text.Json.Serialization.JsonSerializerContext> information, which could manifest as a breaking change for some scenarios.

## Previous behavior

In .NET 6, the following code serializes successfully. The `MyContext` configuration (which doesn't support `Poco2`) is discarded by the copy constructor, and serialization succeeds because the new options instance defaults to using reflection-based serialization.

```csharp
var options = new JsonSerializerOptions(MyContext.Default.Options);
JsonSerializer.Serialize(new Poco2(), options);

[JsonSerializable(typeof(Poco1))]
public partial class MyContext : JsonSerializerContext {}

public class Poco1 {}
public class Poco2 {}
```

## New behavior

Starting in .NET 7, the same code as shown in the [Previous behavior](#previous-behavior) section throws an <xref:System.InvalidOperationException>. That's because the copy constructor now incorporates `MyContext` metadata, which doesn't support `Poco2` contracts.

## Version introduced

.NET 7 Preview 7

## Type of breaking change

This change can affect [binary compatibility](../../categories.md#binary-compatibility).

## Reason for change

<xref:System.Text.Json.Serialization.JsonSerializerContext> was the only setting ignored by the copy constructor. This behavior was surprising for some users.

## Recommended action

If you depend on the .NET 6 behavior, you can manually unset the <xref:System.Text.Json.JsonSerializerOptions.TypeInfoResolver> property to get back reflection-based contract resolution:

```csharp
var options = new JsonSerializerOptions(MyContext.Default.Options);
options.TypeInfoResolver = null; // Unset `MyContext.Default` as the resolver for the options instance.
```

## Affected APIs

- <xref:System.Text.Json.JsonSerializerOptions.%23ctor(System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: "Breaking change: Polymorphic serialization for object types"
description: Learn about the .NET 7 breaking change in serialization where System.Text.Json no longer hardcodes polymorphism for root-level object types.
ms.date: 09/12/2022
---
# Polymorphic serialization for object types

Using default configuration, <xref:System.Text.Json?displayProperty=fullName> serializes values of type `object` [using polymorphism](../../../../standard/serialization/system-text-json-polymorphism.md). This behavior becomes less consistent if you register a custom converter for `object`. `System.Text.Json` has historically hardcoded polymorphism for root-level object values but not for nested object values. Starting with .NET 7 RC 1, this behavior has changed so that custom converters never use polymorphism.

## Previous behavior

Consider the following custom object converter:

```csharp
public class CustomObjectConverter : JsonConverter<object>
{
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
=> writer.WriteNumberValue(42);

public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> throw new NotImplementedException();
}
```

In previous versions, the following code serialized as 0. That's because the serializer used polymorphism and ignored the custom converter.

```csharp
var options = new JsonSerializerOptions { Converters = { new CustomObjectConverter() } };
JsonSerializer.Serialize<object>(0, options);
```

However, the following code serialized as 42 because the serializer honored the custom converter.

```csharp
var options = new JsonSerializerOptions { Converters = { new CustomObjectConverter() } };
JsonSerializer.Serialize<object[]>(new object[] { 0 }, options);
```

## New behavior

Starting in .NET 7, using the custom object converter defined in the [Previous behavior](#previous-behavior) section, the following code serializes as 42. That's because the serializer will always consult the custom converter and not use polymorphism.

```csharp
var options = new JsonSerializerOptions { Converters = { new CustomObjectConverter() } };
JsonSerializer.Serialize<object>(0, options);
```

## Version introduced

.NET 7 RC 1

## Type of breaking change

This change can affect [binary compatibility](../../categories.md#binary-compatibility).

## Reason for change

This change was made due to inconsistent serialization contracts for a type, depending on whether it was being serialized as a root-level value or a nested value.

## Recommended action

If desired, you can get back polymorphism for root-level values by invoking one of the untyped serialization methods:

```csharp
var options = new JsonSerializerOptions { Converters = { new CustomObjectConverter() } };
JsonSerializer.Serialize(0, inputType: typeof(int), options); // Serializes as 0.
```

## Affected APIs

- <xref:System.Text.Json.JsonSerializer.Serialize%60%601(%60%600,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize%60%601(System.IO.Stream,%60%600,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize%60%601(System.Text.Json.Utf8JsonWriter,%60%600,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.SerializeAsync%60%601(System.IO.Stream,%60%600,System.Text.Json.JsonSerializerOptions,System.Threading.CancellationToken)?displayProperty=fullName>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
title: "Breaking change: System.Text.Json source generator fallback"
description: Learn about the .NET 7 breaking change in serialization where the System.Text.Json source generator no longer fall backs to reflection-based serialization for unrecognized types.
ms.date: 09/12/2022
---
# System.Text.Json source generator fallback

When using one of the <xref:System.Text.Json.JsonSerializer> methods that accepts <xref:System.Text.Json.JsonSerializerOptions>, the <xref:System.Text.Json?displayProperty=fullName> source generator will no longer implicitly fall back to reflection-based serialization for unrecognized types.

## Previous behavior

Consider the following source generation example in .NET 6:

```csharp
JsonSerializer.Serialize(new Poco2(), typeof(Poco2), MyContext.Default);

[JsonSerializable(typeof(Poco1))]
public partial class MyContext : JsonSerializerContext {}

public class Poco1 { }
public class Poco2 { }
```

Since `MyContext` does not include `Poco2` in its serializable types, the serialization will correctly fail with the following exception:

```output
System.InvalidOperationException:

'Metadata for type 'Poco2' was not provided to the serializer. The serializer method used does not
support reflection-based creation of serialization-related type metadata. If using source generation,
ensure that all root types passed to the serializer have been indicated with 'JsonSerializableAttribute',
along with any types that might be serialized polymorphically.
```

Now consider the following call, which tries to serialize the same type (`MyContext`) using the <xref:System.Text.Json.JsonSerializerOptions> instance constructed by the source generator:

```csharp
JsonSerializer.Serialize(new Poco2(), MyContext.Default.Options);
```

The options instance silently incorporates the default reflection-based contract resolver as a fallback mechanism, and as such, the type serializes successfully&mdash;using reflection.

The same fallback logic applies to <xref:System.Text.Json.JsonSerializerOptions.GetConverter(System.Type)?displayProperty=nameWithType> for options instances that are attached to a <xref:System.Text.Json.Serialization.JsonSerializerContext>. The following statement will return a converter using the built-in reflection converter:

```csharp
JsonConverter converter = MyContext.Default.Options.GetConverter(typeof(Poco2));
```

## New behavior

Starting in .NET 7, the following call fails with the same exception (<xref:System.InvalidOperationException>) as when using the <xref:System.Text.Json.Serialization.JsonSerializerContext> overload:

```csharp
JsonSerializer.Serialize(new Poco2(), MyContext.Default.Options);
```

In addition, the following statement will fail with a <xref:System.NotSupportedException>:

```csharp
JsonConverter converter = MyContext.Default.Options.GetConverter(typeof(Poco2));
```

## Version introduced

.NET 7 Preview 7

## Type of breaking change

This change can affect [binary compatibility](../../categories.md#binary-compatibility).

## Reason for change

The previous behavior violates the principle of least surprise and ultimately defeats the purpose of source generation. With the release of a feature that allows you to [customize the JSON serialization contracts of your types](https://github.com/dotnet/runtime/issues/63686), you have the ability to fine tune the sources of your contract metadata. With this in mind, silently introducing alternative sources becomes even less desirable.

## Recommended action

You might depend on the previous behavior, either intentionally or unintentionally. As such, you can use the following workaround to continue to fall back to reflection-based serialization when necessary:

```csharp
var options = new JsonSerializerOptions
{
TypeInfoResolver = JsonTypeInfoResolver.Combine(MyContext.Default, new DefaultJsonTypeInfoResolver());
}

JsonSerializer.Serialize(new Poco2(), options); // Contract resolution falls back to the default reflection-based resolver.
options.GetConverter(typeof(Poco2)); // Returns the reflection-based converter.
```

## Affected APIs

- <xref:System.Text.Json.JsonSerializerOptions.GetConverter(System.Type)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize(System.IO.Stream,System.Object,System.Type,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize(System.Object,System.Type,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize(System.Text.Json.Utf8JsonWriter,System.Object,System.Type,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize%60%601(%60%600,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize%60%601(System.IO.Stream,%60%600,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.Serialize%60%601(System.Text.Json.Utf8JsonWriter,%60%600,System.Text.Json.JsonSerializerOptions)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.SerializeAsync(System.IO.Stream,System.Object,System.Type,System.Text.Json.JsonSerializerOptions,System.Threading.CancellationToken)?displayProperty=fullName>
- <xref:System.Text.Json.JsonSerializer.SerializeAsync%60%601(System.IO.Stream,%60%600,System.Text.Json.JsonSerializerOptions,System.Threading.CancellationToken)?displayProperty=fullName>
12 changes: 12 additions & 0 deletions docs/core/compatibility/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ items:
href: serialization/7.0/datacontractserializer-negative-sign.md
- name: Deserialize Version type with leading or trailing whitespace
href: serialization/7.0/deserialize-version-with-whitespace.md
- name: JsonSerializerOptions copy constructor includes JsonSerializerContext
href: serialization/7.0/jsonserializeroptions-copy-constructor.md
- name: Polymorphic serialization for object types
href: serialization/7.0/polymorphic-serialization.md
- name: System.Text.Json source generator fallback
href: serialization/7.0/reflection-fallback.md
- name: XML and XSLT
items:
- name: XmlSecureResolver is obsolete
Expand Down Expand Up @@ -1133,6 +1139,12 @@ items:
href: serialization/7.0/datacontractserializer-negative-sign.md
- name: Deserialize Version type with leading or trailing whitespace
href: serialization/7.0/deserialize-version-with-whitespace.md
- name: JsonSerializerOptions copy constructor includes JsonSerializerContext
href: serialization/7.0/jsonserializeroptions-copy-constructor.md
- name: Polymorphic serialization for object types
href: serialization/7.0/polymorphic-serialization.md
- name: System.Text.Json source generator fallback
href: serialization/7.0/reflection-fallback.md
- name: .NET 6
items:
- name: Default serialization format for TimeSpan
Expand Down