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

JsonIgnoreCondition.WhenWritingNull doesn't play well with required nullable properties #76527

Closed
asleire opened this issue Oct 3, 2022 · 7 comments
Labels
area-System.Text.Json question Answer questions and provide assistance, not an issue with source code or documentation.
Milestone

Comments

@asleire
Copy link

asleire commented Oct 3, 2022

.NET SDK: 7.0.100-rc.1.22431.12

I'm not sure if this is intentional or not, but I'm experiencing an issue when trying to use required nullable properties in my API DTOs.

  1. I want to mark my properties as required in order to force my code to initialize all the properties, even the properties that are nullable.
  2. I want to serialize my objects with the serializer option DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull.

Combining these points lead to an exception upon deserializing a serialized object whose nullable required property was set to null.

I would expect deserialization to ignore required nullable properties when DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull

Minimal reproduction

Code:

using System.Text.Json;
using System.Text.Json.Serialization;

var jsonSerializerOptions = new JsonSerializerOptions()
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};

var serializedFoo = JsonSerializer.Serialize(new Foo() { Bar = null }, jsonSerializerOptions);

// This line throws an exception: JSON deserialization for type 'Foo' was missing required properties, including the following: Bar
JsonSerializer.Deserialize<Foo>(serializedFoo, jsonSerializerOptions);

public class Foo
{
    public required string? Bar { get; set; }
}

Exception:

Unhandled exception. System.Text.Json.JsonException: JSON deserialization for type 'Foo' was missing required properties, including the following: Bar
   at System.Text.Json.ThrowHelper.ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo parent, BitArray requiredPropertiesSet)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Oct 3, 2022
@ghost
Copy link

ghost commented Oct 3, 2022

Tagging subscribers to this area: @dotnet/area-system-text-json, @gregsdennis
See info in area-owners.md if you want to be subscribed.

Issue Details

I'm not sure if this is intentional or not, but I'm experiencing an issue when trying to use required nullable properties in my API DTOs.

  1. I want to mark my properties as required in order to force my code to initialize all the properties, even the properties that are nullable. In order to save space when serializing,
  2. I want to serialize my objects with the serializer option DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull.

Combining these points lead to an exception upon deserializing a serialized object whose nullable required property was set to null.

I would expect deserialization to ignore required nullable properties when DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull

Minimal reproduction

Code:

using System.Text.Json;
using System.Text.Json.Serialization;

var jsonSerializerOptions = new JsonSerializerOptions()
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};

var serializedFoo = JsonSerializer.Serialize(new Foo() { Bar = null }, jsonSerializerOptions);

// This line throws an exception: JSON deserialization for type 'Foo' was missing required properties, including the following: Bar
JsonSerializer.Deserialize<Foo>(serializedFoo, jsonSerializerOptions);

public class Foo
{
    public required string? Bar { get; set; }
}

Exception:

Unhandled exception. System.Text.Json.JsonException: JSON deserialization for type 'Foo' was missing required properties, including the following: Bar
   at System.Text.Json.ThrowHelper.ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo parent, BitArray requiredPropertiesSet)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
Author: asleire
Assignees: -
Labels:

area-System.Text.Json

Milestone: -

@eiriktsarpalis
Copy link
Member

This is by design -- not setting a required property for a newly initialized object violates its contract, so the deserializer will rightly fail if it cannot bind to any JSON property. See the docs on required member support for more details.

The reason you're seeing this error message is that the two configurations are mutually contradictory: it's not possible to roundtrip a value when it's both ignored on serialization and required on deserialization. Suggested workaround would be to override the ignore condition for the specific properties that are marked required:

public class Foo
{
    [JsonIgnore(JsonIgnoreCondition.Never)]
    public required string? Bar { get; set; }
}

@eiriktsarpalis eiriktsarpalis added question Answer questions and provide assistance, not an issue with source code or documentation. and removed untriaged New issue has not been triaged by the area owner labels Oct 3, 2022
@eiriktsarpalis eiriktsarpalis added this to the 8.0.0 milestone Oct 3, 2022
@asleire
Copy link
Author

asleire commented Oct 3, 2022

not setting a required property for a newly initialized object violates its contract, so the deserializer will rightly fail if it cannot bind to any JSON property

Maybe this issue should be changed into a feature request then. The deserializer could be changed so that if a required property is missing, but DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, the deserializer initializes the property to null, thus satisfying the contract

I don't think your workaround is valid as I want to ignore the property when serializing if is it null

@eiriktsarpalis
Copy link
Member

The deserializer could be changed so that if a required property is missing, but DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, the deserializer initializes the property to null

JsonIgnoreCondition.WhenWritingNull strictly influences the serialization contract and has no bearing on how properties are deserialized. Changing the deserializer contradicts both the name and documented semantics of WhenWritingNull and is a breaking change.

I don't think your workaround is valid as I want to ignore the property when serializing if is it null

You should be able to use contract customization to switch off required properties in the context of JSON deserialization. But I wouldn't recommend this approach: by turning off deserialization validation for required properties you end up with instances containing unset properties that the C# compiler assumes have been set. Effectively this violates the contract of your type design.

@asleire
Copy link
Author

asleire commented Oct 3, 2022

Changing the deserializer contradicts both the name and documented semantics of WhenWritingNull and is a breaking change.

A new value for JsonIgnoreCondition or a new JsonSerializerOptions property then? 🙂

I don't think my use case is unreasonable. I want to the required modifier to apply to my C# code, not the corresponding JSON contract. At least for nullable properties.

What about an option in JsonSerializerOptions to control the usage of the required modifier?

Option 1: Make required properties required in the JSON contract (current .NET7 behavior)
Option 2: Make required properties required in the JSON contract for non-nullable properties
Option 3: Ignore the required modifier, making the properties optional (effectively pre-.NET 7 behavior)

@eiriktsarpalis
Copy link
Member

What about an option in JsonSerializerOptions to control the usage of the required modifier?

As mentioned, this is already possible using the contract model.

@eiriktsarpalis
Copy link
Member

Closing as answered.

@ghost ghost locked as resolved and limited conversation to collaborators Nov 17, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests

2 participants