Skip to content

Commit

Permalink
Sourcegen serialization (#31132)
Browse files Browse the repository at this point in the history
  • Loading branch information
gewarren authored Sep 13, 2022
1 parent 7d5d442 commit 2294549
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 0 deletions.
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>
99 changes: 99 additions & 0 deletions docs/core/compatibility/serialization/7.0/reflection-fallback.md
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

0 comments on commit 2294549

Please sign in to comment.